Migrate to Vue3 (#6587)

* Update reaction.vue

* fix  bug

* wip

* wip

* wjio

* wip

* Revert "wip"

This reverts commit e427f2160a.

* wip

* wip

* wip

* Update init.ts

* Update drive-window.vue

* wip

* wip

* Use PascalCase for components

* Use PascalCase for components

* update dep

* wip

* wip

* wip

* Update init.ts

* wip

* Update paging.ts

* Update test.vue

* watch deep

* wip

* lint

* wip

* wip

* wip

* wip

* wiop

* wip

* Update webpack.config.ts

* alllow null poll

* wip

* wip

* wip

* wiop

* UI redesign & refactor (#6714)

* wip

* wip

* wip

* wip

* wip

* Update drive.vue

* Update word-mute.vue

* wip

* wip

* wip

* clean up

* wip

* Update default.vue

* wip

* Update notes.vue

* Update mfm.ts

* Update index.home.vue

* Update post-form.vue

* Update post-form-attaches.vue

* wip

* Update post-form.vue

* Update sidebar.vue

* wip

* wip

* Update index.vue

* wip

* Update default.vue

* Update index.vue

* Update index.vue

* wip

* Update post-form-attaches.vue

* Update note.vue

* wip

* clean up

* Update notes.vue

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* Update index.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update default.vue

* wip

* Update _dark.json5

* wip

* wip

* wip

* clean up

* wip

* wip

* Update index.vue

* Update test.vue

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* clena yop

* wip

* wip

* Update store.ts

* Update messaging-room.vue

* Update default.widgets.vue

* fix

* wip

* wip

* Update modal.vue

* wip

* Update os.ts

* Update os.ts

* Update deck.vue

* Update init.ts

* wip

* Update ja-JP.yml

* v-sizeは単にwindowのresizeを監視するだけで良いかもしれない

* Update modal.vue

* wip

* Update tooltip.ts

* wip

* wip

* wip

* wip

* wip

* Update image-viewer.vue

* wip

* wip

* Update style.scss

* Update style.scss

* Update visitor.vue

* wip

* Update init.ts

* Update init.ts

* wip

* wip

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* wip

* wip

* Update modal.vue

* Update header.vue

* Update menu.vue

* Update about.vue

* Update about-misskey.vue

* wip

* wip

* Update visitor.vue

* Update tooltip.ts

* wip

* Update drive.vue

* wip

* Update style.scss

* Update header.vue

* wip

* wip

* Update users.user.vue

* Update announcements.vue

* wip

* wip

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update style.scss

* Update users.vue

* wip

* Update style.scss

* wip

* Update welcome.entrance.vue

* Update radio.vue

* Update size.ts

* Update emoji-edit-dialog.vue

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* wip

* wip

* wip

* wip

* Update file-dialog.vue

* wip

* wip

* Update token-generate-window.vue

* Update notification-setting-window.vue

* wip

* wip

* Update _error_.vue

* Update ja-JP.yml

* wip

* wip

* Update store.ts

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* Update announcements.vue

* Update store.ts

* wip

* Update page-editor.vue

* wip

* wip

* Update modal.vue

* wip

* Update select-file.ts

* Update timeline.vue

* Update emojis.vue

* Update os.ts

* wip

* Update user-select.vue

* Update mfm.ts

* Update get-file-info.ts

* Update drive.vue

* Update init.ts

* Update mfm.ts

* wip

* wip

* Update window.vue

* Update note.vue

* wip

* wip

* Update user-info.vue

* wip

* wip

* wip

* wip

* wip

* Update header.vue

* Update header.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update webpack.config.ts

* wip

* wip

* wip

* wip

* wip

* wip

* Update autocomplete.ts

* wip

* wip

* wip

* Update toast.vue

* wip

* Update post-form-dialog.vue

* wip

* wip

* wip

* wip

* wip

* Update users.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update package.json

* wip

* Update icon-dialog.vue

* wip

* wip

* Update user-preview.ts

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* Update user-name.vue

* Update federation.vue

* Update instance.vue

* wip

* wip

* Update tag.vue

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* wip

* Update os.ts

* Update os.ts

* wip

* wip

* wip

* Update router.ts

* wip

* Update init.ts

* Update note.vue

* Update messages.vue

* wip

* wip

* wip

* wip

* wip

* google

* wip

* wip

* wip

* wip

* Update theme-editor.vue

* wip

* wip

* Update room.vue

* Update channel-editor.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update menu.vue

* wip

* wip

* wip

* wip

* Update messaging-room.vue

* wip

* Update post-form.vue

* Update default.widgets.vue

* Update window.vue

* wip
This commit is contained in:
syuilo
2020-10-17 20:12:00 +09:00
committed by GitHub
parent a40f38b2b5
commit 7199e6f4e0
357 changed files with 15053 additions and 12496 deletions

View File

@@ -1,44 +1,41 @@
<template>
<div class="ztgjmzrw">
<portal to="icon"><fa :icon="faBroadcastTower"/></portal>
<portal to="title">{{ $t('announcements') }}</portal>
<mk-button @click="add()" primary style="margin: 0 auto 16px auto;"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
<section class="_card announcements">
<div class="_content announcement" v-for="announcement in announcements">
<mk-input v-model="announcement.title">
<span>{{ $t('title') }}</span>
</mk-input>
<mk-textarea v-model="announcement.text">
<span>{{ $t('text') }}</span>
</mk-textarea>
<mk-input v-model="announcement.imageUrl">
<span>{{ $t('imageUrl') }}</span>
</mk-input>
<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
<div class="buttons">
<mk-button class="button" inline @click="save(announcement)" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<mk-button class="button" inline @click="remove(announcement)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button>
</div>
<div class="_section">
<div class="_content">
<MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
<section class="_card _vMargin announcements" v-for="announcement in announcements">
<div class="_content announcement">
<MkInput v-model:value="announcement.title">
<span>{{ $t('title') }}</span>
</MkInput>
<MkTextarea v-model:value="announcement.text">
<span>{{ $t('text') }}</span>
</MkTextarea>
<MkInput v-model:value="announcement.imageUrl">
<span>{{ $t('imageUrl') }}</span>
</MkInput>
<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
<div class="buttons">
<MkButton class="button" inline @click="save(announcement)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
<MkButton class="button" inline @click="remove(announcement)"><Fa :icon="faTrashAlt"/> {{ $t('remove') }}</MkButton>
</div>
</div>
</section>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faBroadcastTower, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkTextarea from '../../components/ui/textarea.vue';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('announcements') as string
};
},
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkInput,
@@ -47,13 +44,19 @@ export default Vue.extend({
data() {
return {
INFO: {
header: [{
title: this.$t('announcements'),
icon: faBroadcastTower
}]
},
announcements: [],
faBroadcastTower, faSave, faTrashAlt, faPlus
}
},
created() {
this.$root.api('admin/announcements/list').then(announcements => {
os.api('admin/announcements/list').then(announcements => {
this.announcements = announcements;
});
},
@@ -69,38 +72,38 @@ export default Vue.extend({
},
remove(announcement) {
this.$root.dialog({
os.dialog({
type: 'warning',
text: this.$t('removeAreYouSure', { x: announcement.title }),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.announcements = this.announcements.filter(x => x != announcement);
this.$root.api('admin/announcements/delete', announcement);
os.api('admin/announcements/delete', announcement);
});
},
save(announcement) {
if (announcement.id == null) {
this.$root.api('admin/announcements/create', announcement).then(() => {
this.$root.dialog({
os.api('admin/announcements/create', announcement).then(() => {
os.dialog({
type: 'success',
text: this.$t('saved')
});
}).catch(e => {
this.$root.dialog({
os.dialog({
type: 'error',
text: e
});
});
} else {
this.$root.api('admin/announcements/update', announcement).then(() => {
this.$root.dialog({
os.api('admin/announcements/update', announcement).then(() => {
os.dialog({
type: 'success',
text: this.$t('saved')
});
}).catch(e => {
this.$root.dialog({
os.dialog({
type: 'error',
text: e
});
@@ -110,17 +113,3 @@ export default Vue.extend({
}
});
</script>
<style lang="scss" scoped>
.ztgjmzrw {
> .announcements {
> .announcement {
> .buttons {
> .button:first-child {
margin-right: 8px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,116 @@
<template>
<XModalWindow ref="dialog"
:width="370"
:with-ok-button="true"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
@ok="ok()"
>
<template #header>:{{ emoji.name }}:</template>
<div class="yigymqpb _section">
<img :src="emoji.url" class="img"/>
<MkInput v-model:value="name"><span>{{ $t('name') }}</span></MkInput>
<MkInput v-model:value="category" :datalist="categories"><span>{{ $t('category') }}</span></MkInput>
<MkInput v-model:value="aliases">
<span>{{ $t('tags') }}</span>
<template #desc>{{ $t('setMultipleBySeparatingWithSpace') }}</template>
</MkInput>
<MkButton danger @click="del()"><Fa :icon="faTrashAlt"/> {{ $t('delete') }}</MkButton>
</div>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import * as os from '@/os';
import { unique } from '../../../prelude/array';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
props: {
emoji: {
required: true,
}
},
emits: ['done', 'closed'],
data() {
return {
name: this.emoji.name,
category: this.emoji.category,
aliases: this.emoji.aliases?.join(' '),
categories: [],
faTrashAlt,
}
},
created() {
os.api('meta', { detail: false }).then(({ emojis }) => {
this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
});
},
methods: {
ok() {
this.update();
},
async update() {
await os.apiWithDialog('admin/emoji/update', {
id: this.emoji.id,
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
});
this.$emit('done', {
updated: {
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
}
});
this.$refs.dialog.close();
},
async del() {
const { canceled } = await os.dialog({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.emoji.name }),
showCancelButton: true
});
if (canceled) return;
os.api('admin/emoji/remove', {
id: this.emoji.id
}).then(() => {
this.$emit('done', {
deleted: true
});
this.$refs.dialog.close();
});
},
}
});
</script>
<style lang="scss" scoped>
.yigymqpb {
> .img {
display: block;
height: 64px;
margin: 0 auto;
}
}
</style>

View File

@@ -1,80 +1,67 @@
<template>
<div class="mk-instance-emojis">
<portal to="icon"><fa :icon="faLaugh"/></portal>
<portal to="title">{{ $t('customEmojis') }}</portal>
<div class="_section" style="padding: 0;">
<MkTab v-model:value="tab" :items="[{ label: $t('local'), value: 'local' }, { label: $t('remote'), value: 'remote' }]"/>
</div>
<section class="_card _vMargin local">
<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div>
<div class="_content">
<mk-pagination :pagination="pagination" class="emojis" ref="emojis">
<div class="_section">
<div class="_content local" v-if="tab === 'local'">
<MkButton primary @click="add" style="margin: 0 auto var(--margin) auto;"><Fa :icon="faPlus"/> {{ $t('addEmoji') }}</MkButton>
<MkInput v-model:value="query" :debounce="true" type="search"><template #icon><Fa :icon="faSearch"/></template><span>{{ $t('search') }}</span></MkInput>
<MkPagination :pagination="pagination" ref="emojis">
<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
<template #default="{items}">
<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" @click="selected = emoji" :class="{ selected: selected && (selected.id === emoji.id) }">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<span class="name">{{ emoji.name }}</span>
<span class="info">
<b class="category">{{ emoji.category }}</b>
<span class="aliases">{{ emoji.aliases.join(' ') }}</span>
</span>
<div class="emojis">
<button class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<span class="name">{{ emoji.name }}</span>
<span class="info">
<span class="category">{{ emoji.category }}</span>
</span>
</div>
</button>
</div>
</template>
</MkPagination>
</div>
<div class="_content remote" v-else-if="tab === 'remote'">
<MkInput v-model:value="queryRemote" :debounce="true" type="search"><template #icon><Fa :icon="faSearch"/></template><span>{{ $t('search') }}</span></MkInput>
<MkInput v-model:value="host" :debounce="true"><span>{{ $t('host') }}</span></MkInput>
<MkPagination :pagination="remotePagination" ref="remoteEmojis">
<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
<template #default="{items}">
<div class="emojis">
<div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<span class="name">{{ emoji.name }}</span>
<span class="info">{{ emoji.host }}</span>
</div>
</div>
</div>
</template>
</mk-pagination>
</MkPagination>
</div>
<div class="_content" v-if="selected">
<mk-input v-model="name"><span>{{ $t('name') }}</span></mk-input>
<mk-input v-model="category" :datalist="categories"><span>{{ $t('category') }}</span></mk-input>
<mk-input v-model="aliases"><span>{{ $t('tags') }}</span></mk-input>
<mk-button inline primary @click="update"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button>
</div>
<div class="_footer">
<mk-button inline primary @click="add"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
</div>
</section>
<section class="_card _vMargin remote">
<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div>
<div class="_content">
<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
<mk-pagination :pagination="remotePagination" class="emojis" ref="remoteEmojis">
<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
<template #default="{items}">
<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" @click="selectedRemote = emoji" :class="{ selected: selectedRemote && (selectedRemote.id === emoji.id) }">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<span class="name">{{ emoji.name }}</span>
<span class="info">{{ emoji.host }}</span>
</div>
</div>
</template>
</mk-pagination>
</div>
<div class="_footer">
<mk-button inline primary :disabled="selectedRemote == null" @click="im()"><fa :icon="faPlus"/> {{ $t('import') }}</mk-button>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPlus, faSave } from '@fortawesome/free-solid-svg-icons';
import { computed, defineComponent } from 'vue';
import { faPlus, faSave, faSearch } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faLaugh } from '@fortawesome/free-regular-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkPagination from '../../components/ui/pagination.vue';
import { selectFile } from '../../scripts/select-file';
import { unique } from '../../../prelude/array';
export default Vue.extend({
metaInfo() {
return {
title: `${this.$t('customEmojis')} | ${this.$t('instance')}`
};
},
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkTab from '@/components/tab.vue';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
export default defineComponent({
components: {
MkTab,
MkButton,
MkInput,
MkPagination,
@@ -82,54 +69,44 @@ export default Vue.extend({
data() {
return {
selected: null,
selectedRemote: null,
name: null,
category: null,
aliases: null,
INFO: {
header: [{
title: this.$t('customEmojis'),
icon: faLaugh
}],
action: {
icon: faPlus,
handler: this.add
}
},
tab: 'local',
query: null,
queryRemote: null,
host: '',
pagination: {
endpoint: 'admin/emoji/list',
limit: 10,
limit: 15,
params: computed(() => ({
query: (this.query && this.query !== '') ? this.query : null
}))
},
remotePagination: {
endpoint: 'admin/emoji/list-remote',
limit: 10,
params: () => ({
host: this.host ? this.host : null
})
limit: 15,
params: computed(() => ({
query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
host: (this.host && this.host !== '') ? this.host : null
}))
},
faTrashAlt, faPlus, faLaugh, faSave
}
},
computed: {
categories() {
if (this.$store.state.instance.meta) {
return unique(this.$store.state.instance.meta.emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
} else {
return [];
}
}
},
watch: {
host() {
this.$refs.remoteEmojis.reload();
},
selected() {
this.name = this.selected ? this.selected.name : null;
this.category = this.selected ? this.selected.category : null;
this.aliases = this.selected ? this.selected.aliases.join(' ') : null;
faTrashAlt, faPlus, faLaugh, faSave, faSearch,
}
},
methods: {
async add(e) {
const files = await selectFile(this, e.currentTarget || e.target, null, true);
const files = await selectFile(e.currentTarget || e.target, null, true);
const dialog = this.$root.dialog({
const dialog = os.dialog({
type: 'waiting',
text: this.$t('doing') + '...',
showOkButton: false,
@@ -137,133 +114,112 @@ export default Vue.extend({
cancelableByBgClick: false
});
Promise.all(files.map(file => this.$root.api('admin/emoji/add', {
Promise.all(files.map(file => os.api('admin/emoji/add', {
fileId: file.id,
})))
.then(() => {
this.$refs.emojis.reload();
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
os.success();
})
.finally(() => {
dialog.close();
dialog.cancel();
});
},
async update() {
await this.$root.api('admin/emoji/update', {
id: this.selected.id,
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
});
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$refs.emojis.reload();
async edit(emoji) {
os.popup(await import('./emoji-edit-dialog.vue'), {
emoji: emoji
}, {
done: result => {
if (result.updated) {
this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
...emoji,
...result.updated
});
} else if (result.deleted) {
this.$refs.emojis.removeItem(item => item.id === emoji.id);
}
},
}, 'closed');
},
async del() {
const { canceled } = await this.$root.dialog({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.selected.name }),
showCancelButton: true
});
if (canceled) return;
this.$root.api('admin/emoji/remove', {
id: this.selected.id
}).then(() => {
this.$refs.emojis.reload();
im(emoji) {
os.apiWithDialog('admin/emoji/copy', {
emojiId: emoji.id,
});
},
im() {
this.$root.api('admin/emoji/copy', {
emojiId: this.selectedRemote.id,
}).then(() => {
this.$refs.emojis.reload();
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
remoteMenu(emoji, ev) {
os.modalMenu([{
type: 'label',
text: ':' + emoji.name + ':',
}, {
text: this.$t('import'),
icon: faPlus,
action: () => { this.im(emoji) }
}], ev.currentTarget || ev.target);
}
}
});
</script>
<style lang="scss" scoped>
.mk-instance-emojis {
> .local {
> ._content {
max-height: 300px;
overflow: auto;
> .emojis {
> ._section {
> .local {
.emojis {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: var(--margin);
> .emoji {
display: flex;
align-items: center;
padding: 12px;
text-align: left;
&.selected {
background: var(--accent);
box-shadow: 0 0 0 8px var(--accent);
color: #fff;
&:hover {
color: var(--accent);
}
> .img {
width: 50px;
height: 50px;
width: 42px;
height: 42px;
}
> .body {
padding: 8px;
padding: 0 0 0 8px;
white-space: nowrap;
overflow: hidden;
> .name {
display: block;
text-overflow: ellipsis;
overflow: hidden;
}
> .info {
opacity: 0.5;
> .category {
margin-right: 16px;
}
> .aliases {
font-style: oblique;
}
}
}
}
}
}
}
> .remote {
> ._content {
max-height: 300px;
overflow: auto;
> .emojis {
> .remote {
.emojis {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: var(--margin);
> .emoji {
display: flex;
align-items: center;
padding: 12px;
text-align: left;
&.selected {
background: var(--accent);
box-shadow: 0 0 0 8px var(--accent);
color: #fff;
&:hover {
color: var(--accent);
}
> .img {
@@ -272,14 +228,21 @@ export default Vue.extend({
}
> .body {
padding: 0 8px;
padding: 0 0 0 8px;
white-space: nowrap;
overflow: hidden;
> .name {
display: block;
text-overflow: ellipsis;
overflow: hidden;
}
> .info {
opacity: 0.5;
display: block;
text-overflow: ellipsis;
overflow: hidden;
}
}
}

View File

@@ -1,13 +1,10 @@
<template>
<div class="mk-federation">
<portal to="icon"><fa :icon="faGlobe"/></portal>
<portal to="title">{{ $t('federation') }}</portal>
<section class="_card instances">
<div>
<div class="_section">
<div class="_content">
<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
<MkInput v-model:value="host" :debounce="true"><span>{{ $t('host') }}</span></MkInput>
<div class="inputs" style="display: flex;">
<mk-select v-model="state" style="margin: 0; flex: 1;">
<MkSelect v-model:value="state" style="margin: 0; flex: 1;">
<template #label>{{ $t('state') }}</template>
<option value="all">{{ $t('all') }}</option>
<option value="federating">{{ $t('federating') }}</option>
@@ -16,8 +13,8 @@
<option value="suspended">{{ $t('suspended') }}</option>
<option value="blocked">{{ $t('blocked') }}</option>
<option value="notResponding">{{ $t('notResponding') }}</option>
</mk-select>
<mk-select v-model="sort" style="margin: 0; flex: 1;">
</MkSelect>
<MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
<template #label>{{ $t('sort') }}</template>
<option value="+pubSub">{{ $t('pubSub') }} ({{ $t('descendingOrder') }})</option>
<option value="-pubSub">{{ $t('pubSub') }} ({{ $t('ascendingOrder') }})</option>
@@ -37,44 +34,41 @@
<option value="-driveUsage">{{ $t('driveUsage') }} ({{ $t('ascendingOrder') }})</option>
<option value="+driveFiles">{{ $t('driveFiles') }} ({{ $t('descendingOrder') }})</option>
<option value="-driveFiles">{{ $t('driveFiles') }} ({{ $t('ascendingOrder') }})</option>
</mk-select>
</MkSelect>
</div>
</div>
</div>
<div class="_section">
<div class="_content">
<mk-pagination :pagination="pagination" #default="{items}" class="instances" ref="instances" :key="host + state">
<div class="instance" v-for="instance in items" :key="instance.id" @click="info(instance)">
<div class="host"><fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
<MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
<div class="ppgwaixt _panel" v-for="instance in items" :key="instance.id" @click="info(instance)">
<div class="host"><Fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
<div class="status">
<span class="sub" v-if="instance.followersCount > 0"><fa :icon="faCaretDown" class="icon"/>Sub</span>
<span class="sub" v-else><fa :icon="faCaretDown" class="icon"/>-</span>
<span class="pub" v-if="instance.followingCount > 0"><fa :icon="faCaretUp" class="icon"/>Pub</span>
<span class="pub" v-else><fa :icon="faCaretUp" class="icon"/>-</span>
<span class="lastCommunicatedAt"><fa :icon="faExchangeAlt" class="icon"/><mk-time :time="instance.lastCommunicatedAt"/></span>
<span class="latestStatus"><fa :icon="faTrafficLight" class="icon"/>{{ instance.latestStatus || '-' }}</span>
<span class="sub" v-if="instance.followersCount > 0"><Fa :icon="faCaretDown" class="icon"/>Sub</span>
<span class="sub" v-else><Fa :icon="faCaretDown" class="icon"/>-</span>
<span class="pub" v-if="instance.followingCount > 0"><Fa :icon="faCaretUp" class="icon"/>Pub</span>
<span class="pub" v-else><Fa :icon="faCaretUp" class="icon"/>-</span>
<span class="lastCommunicatedAt"><Fa :icon="faExchangeAlt" class="icon"/><MkTime :time="instance.lastCommunicatedAt"/></span>
<span class="latestStatus"><Fa :icon="faTrafficLight" class="icon"/>{{ instance.latestStatus || '-' }}</span>
</div>
</div>
</mk-pagination>
</MkPagination>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faGlobe, faCircle, faExchangeAlt, faCaretDown, faCaretUp, faTrafficLight } from '@fortawesome/free-solid-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkSelect from '../../components/ui/select.vue';
import MkPagination from '../../components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkSelect from '@/components/ui/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkInstanceInfo from './instance.vue';
import * as os from '@/os';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('federation') as string
};
},
export default defineComponent({
components: {
MkButton,
MkInput,
@@ -84,6 +78,12 @@ export default Vue.extend({
data() {
return {
INFO: {
header: [{
title: this.$t('federation'),
icon: faGlobe
}],
},
host: '',
state: 'federating',
sort: '+pubSub',
@@ -125,60 +125,57 @@ export default Vue.extend({
},
info(instance) {
this.$root.new(MkInstanceInfo, {
os.popup(MkInstanceInfo, {
instance: instance
});
}, {}, 'closed');
}
}
});
</script>
<style lang="scss" scoped>
.mk-federation {
> .instances {
> ._content {
> .instances {
> .instance {
cursor: pointer;
.ppgwaixt {
cursor: pointer;
padding: 16px;
> .host {
> .indicator {
font-size: 70%;
vertical-align: baseline;
margin-right: 4px;
&:hover {
color: var(--accent);
}
&.green {
color: #49c5ba;
}
> .host {
> .indicator {
font-size: 70%;
vertical-align: baseline;
margin-right: 4px;
&.yellow {
color: #c5a549;
}
&.green {
color: #49c5ba;
}
&.red {
color: #c54949;
}
&.yellow {
color: #c5a549;
}
&.off {
color: rgba(0, 0, 0, 0.5);
}
}
}
&.red {
color: #c54949;
}
> .status {
display: flex;
align-items: center;
font-size: 90%;
&.off {
color: rgba(0, 0, 0, 0.5);
}
}
}
> span {
flex: 1;
> .icon {
margin-right: 6px;
}
}
}
}
> .status {
display: flex;
align-items: center;
font-size: 90%;
> span {
flex: 1;
> .icon {
margin-right: 6px;
}
}
}

View File

@@ -0,0 +1,136 @@
<template>
<XModalWindow ref="dialog"
:width="370"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template #header v-if="file">{{ file.name }}</template>
<div class="cxqhhsmd" v-if="file">
<div class="_section">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div class="info">
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
<MkTime :time="file.createdAt" mode="detail" style="display: block;"/>
</div>
</div>
<div class="_section">
<div class="_content">
<MkSwitch @update:value="toggleIsSensitive" v-model:value="isSensitive">NSFW</MkSwitch>
</div>
</div>
<div class="_section">
<div class="_content">
<MkButton full @click="showUser"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('user') }}</MkButton>
<MkButton full danger @click="del"><Fa :icon="faTrashAlt"/> {{ $t('delete') }}</MkButton>
</div>
</div>
<div class="_section" v-if="info">
<details class="_content rawdata">
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
</details>
</div>
</div>
</XModalWindow>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake, faTrashAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import Progress from '@/scripts/loading';
import bytes from '@/filters/bytes';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
XModalWindow,
MkDriveFileThumbnail,
},
props: {
fileId: {
required: true,
}
},
emits: ['closed'],
data() {
return {
file: null,
info: null,
isSensitive: false,
faTimes, faBookmark, farBookmark, faKey, faSync, faMicrophoneSlash, faSnowflake, faTrashAlt, faExternalLinkSquareAlt
};
},
created() {
this.fetch();
},
methods: {
async fetch() {
Progress.start();
this.file = await os.api('drive/files/show', { fileId: this.fileId });
this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
this.isSensitive = this.file.isSensitive;
Progress.done();
},
async showUser() {
os.popup(await import('./user-dialog.vue'), {
userId: this.file.userId
}, {}, 'closed');
},
async del() {
const { canceled } = await os.dialog({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.file.name }),
showCancelButton: true
});
if (canceled) return;
os.api('drive/files/delete', {
fileId: this.file.id
}).then(() => {
this.$refs.files.removeItem(x => x.id === this.file.id);
});
},
async toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
this.isSensitive = v;
},
bytes
}
});
</script>
<style lang="scss" scoped>
.cxqhhsmd {
> ._section {
> .thumbnail {
height: 150px;
max-width: 100%;
}
> .info {
text-align: center;
margin-top: 8px;
}
> .rawdata {
overflow: auto;
}
}
}
</style>

View File

@@ -1,54 +1,190 @@
<template>
<section class="_card">
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<mk-button primary @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearCachedFiles') }}</mk-button>
<div class="xrmjdkdw">
<div class="_section">
<div class="_content">
<MkButton primary @click="clear()"><Fa :icon="faTrashAlt"/> {{ $t('clearCachedFiles') }}</MkButton>
</div>
</div>
</section>
<div class="_section lookup">
<div class="_title"><Fa :icon="faSearch"/> {{ $t('lookup') }}</div>
<div class="_content">
<MkInput class="target" v-model:value="q" type="text" @enter="find()">
<span>{{ $t('fileIdOrUrl') }}</span>
</MkInput>
<MkButton @click="find()" primary><Fa :icon="faSearch"/> {{ $t('lookup') }}</MkButton>
</div>
</div>
<div class="_section">
<div class="_content">
<div class="inputs" style="display: flex;">
<MkSelect v-model:value="origin" style="margin: 0; flex: 1;">
<template #label>{{ $t('instance') }}</template>
<option value="combined">{{ $t('all') }}</option>
<option value="local">{{ $t('local') }}</option>
<option value="remote">{{ $t('remote') }}</option>
</MkSelect>
<MkInput v-model:value="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
<span>{{ $t('host') }}</span>
</MkInput>
</div>
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model:value="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
<span>{{ $t('type') }}</span>
</MkInput>
</div>
<MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files" :auto-margin="false">
<button class="file _panel _button _vMargin" v-for="file in items" :key="file.id" @click="show(file, $event)">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div class="body">
<div>
<small style="opacity: 0.7;">{{ file.name }}</small>
</div>
<div>
<MkAcct :user="file.user"/>
</div>
<div>
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
</div>
<div>
<span>{{ $t('registeredDate') }}: <MkTime :time="file.createdAt" mode="detail"/></span>
</div>
</div>
</button>
</MkPagination>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faCloud } from '@fortawesome/free-solid-svg-icons';
import { defineComponent } from 'vue';
import { faCloud, faSearch } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkPagination from '../../components/ui/pagination.vue';
export default Vue.extend({
metaInfo() {
return {
title: `${this.$t('files')} | ${this.$t('instance')}`
};
},
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkSelect from '@/components/ui/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkInput,
MkSelect,
MkPagination,
MkDriveFileThumbnail,
},
data() {
return {
faTrashAlt, faCloud
INFO: {
header: [{
title: this.$t('files'),
icon: faCloud
}],
},
q: null,
origin: 'local',
type: null,
searchHost: '',
pagination: {
endpoint: 'admin/drive/files',
limit: 10,
params: () => ({
type: (this.type && this.type !== '') ? this.type : null,
origin: this.origin,
hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
}),
},
faTrashAlt, faCloud, faSearch,
}
},
watch: {
type() {
this.$refs.files.reload();
},
origin() {
this.$refs.files.reload();
},
searchHost() {
this.$refs.files.reload();
},
},
methods: {
clear() {
this.$root.dialog({
os.dialog({
type: 'warning',
text: this.$t('clearCachedFilesConfirm'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('admin/drive/clean-remote-files', {}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
os.apiWithDialog('admin/drive/clean-remote-files', {});
});
}
},
async show(file, ev) {
os.popup(await import('./file-dialog.vue'), {
fileId: file.id
}, {}, 'closed');
},
find() {
os.api('admin/drive/show-file', this.q.startsWith('http://') || this.q.startsWith('https://') ? { url: this.q.trim() } : { fileId: this.q.trim() }).then(file => {
this.show(file);
}).catch(e => {
if (e.code === 'NO_SUCH_FILE') {
os.dialog({
type: 'error',
text: this.$t('notFound')
});
}
});
},
bytes
}
});
</script>
<style lang="scss" scoped>
.xrmjdkdw {
.urempief {
margin-top: var(--margin);
> .file {
display: flex;
width: 100%;
box-sizing: border-box;
text-align: left;
align-items: center;
&:hover {
color: var(--accent);
}
> .thumbnail {
width: 128px;
height: 128px;
}
> .body {
margin-left: 0.3em;
padding: 8px;
flex: 1;
@media (max-width: 500px) {
font-size: 14px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,576 @@
<template>
<div>
<MkFolder>
<template #header><Fa :icon="faHeartbeat"/> {{ $t('metrics') }}</template>
<div class="_section" style="padding: 0 var(--margin);">
<div class="_content">
<MkContainer :body-togglable="false" class="_vMargin">
<template #header><Fa :icon="faMicrochip"/>{{ $t('cpuAndMemory') }}</template>
<!--
<template #func>
<button class="_button" @click="resume" :disabled="!paused"><Fa :icon="faPlay"/></button>
<button class="_button" @click="pause" :disabled="paused"><Fa :icon="faPause"/></button>
</template>
-->
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas :ref="cpumem"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="_table">
<div class="_row">
<div class="_cell"><div class="_label">MEM total</div>{{ bytes(serverInfo.mem.total) }}</div>
<div class="_cell"><div class="_label">MEM used</div>{{ bytes(memUsage) }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
<div class="_cell"><div class="_label">MEM free</div>{{ bytes(serverInfo.mem.total - memUsage) }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</MkContainer>
<MkContainer :body-togglable="false" class="_vMargin">
<template #header><Fa :icon="faHdd"/> {{ $t('disk') }}</template>
<!--
<template #func>
<button class="_button" @click="resume" :disabled="!paused"><Fa :icon="faPlay"/></button>
<button class="_button" @click="pause" :disabled="paused"><Fa :icon="faPause"/></button>
</template>
-->
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas :ref="disk"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="_table">
<div class="_row">
<div class="_cell"><div class="_label">Disk total</div>{{ bytes(serverInfo.fs.total) }}</div>
<div class="_cell"><div class="_label">Disk used</div>{{ bytes(serverInfo.fs.used) }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
<div class="_cell"><div class="_label">Disk free</div>{{ bytes(serverInfo.fs.total - serverInfo.fs.used) }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</MkContainer>
<MkContainer :body-togglable="false" class="_vMargin">
<template #header><Fa :icon="faExchangeAlt"/> {{ $t('network') }}</template>
<!--
<template #func>
<button class="_button" @click="resume" :disabled="!paused"><Fa :icon="faPlay"/></button>
<button class="_button" @click="pause" :disabled="paused"><Fa :icon="faPause"/></button>
</template>
-->
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas :ref="net"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="_table">
<div class="_row">
<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
</div>
</div>
</div>
</MkContainer>
</div>
</div>
</MkFolder>
<MkFolder>
<template #header><Fa :icon="faClipboardList"/> {{ $t('jobQueue') }}</template>
<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }">
<MkContainer :body-togglable="false" :scrollable="true" :resize-base-el="() => $el">
<template #header><Fa :icon="faExclamationTriangle"/> {{ $t('delayed') }}</template>
<div class="_content">
<div class="_keyValue" v-for="job in jobs" :key="job[0]">
<button class="_button" @click="showInstanceInfo(job[0])">{{ job[0] }}</button>
<div style="text-align: right;">{{ number(job[1]) }} jobs</div>
</div>
</div>
</MkContainer>
<XQueue :connection="queueConnection" domain="inbox" ref="queue" class="queue">
<template #title><Fa :icon="faExchangeAlt"/> In</template>
</XQueue>
<XQueue :connection="queueConnection" domain="deliver" class="queue">
<template #title><Fa :icon="faExchangeAlt"/> Out</template>
</XQueue>
</div>
</MkFolder>
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import { faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/ui/select.vue';
import MkInput from '@/components/ui/input.vue';
import MkContainer from '@/components/ui/container.vue';
import MkFolder from '@/components/ui/folder.vue';
import MkwFederation from '../../widgets/federation.vue';
import { version, url } from '@/config';
import bytes from '../../filters/bytes';
import number from '../../filters/number';
import MkInstanceInfo from './instance.vue';
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 '@/os';
export default defineComponent({
components: {
MkButton,
MkSelect,
MkInput,
MkContainer,
MkFolder,
MkwFederation,
},
data() {
return {
version,
url,
stats: null,
serverInfo: null,
connection: null,
queueConnection: os.stream.useSharedConnection('queueStats'),
memUsage: 0,
chartCpuMem: null,
chartNet: null,
jobs: [],
logs: [],
logLevel: 'all',
logDomain: '',
modLogs: [],
dbInfo: null,
overviewHeight: '1fr',
queueHeight: '1fr',
paused: false,
faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList,
}
},
computed: {
gridColor() {
// TODO: var(--panel)の色が暗いか明るいかで判定する
return this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
},
},
mounted() {
this.fetchJobs();
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
os.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
this.connection = os.stream.useSharedConnection('serverStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 150
});
this.$nextTick(() => {
this.queueConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
});
});
},
beforeUnmount() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
this.connection.dispose();
this.queueConnection.dispose();
},
methods: {
cpumem(el) {
if (this.chartCpuMem != null) return;
this.chartCpuMem = markRaw(new Chart(el, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#86b300',
backgroundColor: alpha('#86b300', 0.1),
data: []
}, {
label: 'MEM (active)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
data: []
}, {
label: 'MEM (used)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false,
color: this.gridColor,
zeroLineColor: this.gridColor,
},
ticks: {
display: false,
}
}],
yAxes: [{
position: 'right',
gridLines: {
display: true,
color: this.gridColor,
zeroLineColor: this.gridColor,
},
ticks: {
display: false,
max: 100
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
}));
},
net(el) {
if (this.chartNet != null) return;
this.chartNet = markRaw(new Chart(el, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'In',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Out',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false,
color: this.gridColor,
zeroLineColor: this.gridColor,
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
gridLines: {
display: true,
color: this.gridColor,
zeroLineColor: this.gridColor,
},
ticks: {
display: false,
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
}));
},
disk(el) {
if (this.chartDisk != null) return;
this.chartDisk = markRaw(new Chart(el, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Read',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Write',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false,
color: this.gridColor,
zeroLineColor: this.gridColor,
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
gridLines: {
display: true,
color: this.gridColor,
zeroLineColor: this.gridColor,
},
ticks: {
display: false,
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
}));
},
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() {
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
this.jobs = jobs;
});
},
onStats(stats) {
if (this.paused) return;
const cpu = (stats.cpu * 100).toFixed(0);
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
this.memUsage = stats.mem.active;
this.chartCpuMem.data.labels.push('');
this.chartCpuMem.data.datasets[0].data.push(cpu);
this.chartCpuMem.data.datasets[1].data.push(memActive);
this.chartCpuMem.data.datasets[2].data.push(memUsed);
this.chartNet.data.labels.push('');
this.chartNet.data.datasets[0].data.push(stats.net.rx);
this.chartNet.data.datasets[1].data.push(stats.net.tx);
this.chartDisk.data.labels.push('');
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
this.chartCpuMem.data.labels.shift();
this.chartCpuMem.data.datasets[0].data.shift();
this.chartCpuMem.data.datasets[1].data.shift();
this.chartCpuMem.data.datasets[2].data.shift();
this.chartNet.data.labels.shift();
this.chartNet.data.datasets[0].data.shift();
this.chartNet.data.datasets[1].data.shift();
this.chartDisk.data.labels.shift();
this.chartDisk.data.datasets[0].data.shift();
this.chartDisk.data.datasets[1].data.shift();
}
this.chartCpuMem.update();
this.chartNet.update();
this.chartDisk.update();
},
onStatsLog(statsLog) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},
bytes,
number,
pause() {
this.paused = true;
},
resume() {
this.paused = false;
},
}
});
</script>
<style lang="scss" scoped>
.xhexznfu {
&.min-width_1000px {
.sboqnrfi {
display: grid;
grid-template-columns: 3.2fr 1fr;
grid-template-rows: 1fr;
gap: 16px 16px;
> .stats {
height: min-content;
}
> .column {
display: flex;
flex-direction: column;
> .info {
flex-shrink: 0;
flex-grow: 0;
}
> .db {
flex: 1;
flex-grow: 0;
height: 100%;
}
> .fed {
flex: 1;
flex-grow: 0;
height: 100%;
}
> *:not(:last-child) {
margin-bottom: var(--margin);
}
}
}
.segusily {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr;
gap: 16px 16px;
padding: 0 16px;
}
.vkyrmkwb {
display: grid;
grid-template-columns: 0.5fr 1fr 1fr;
grid-template-rows: 1fr;
gap: 16px 16px;
margin-bottom: var(--margin);
> .queue {
height: min-content;
}
> * {
margin-bottom: 0;
}
}
.uwuemslx {
display: grid;
grid-template-columns: 2fr 3fr;
grid-template-rows: 1fr;
gap: 16px 16px;
height: 400px;
}
}
.vkyrmkwb {
> * {
margin-bottom: var(--margin);
}
}
}
</style>

View File

@@ -1,198 +0,0 @@
<template>
<mk-container :body-togglable="false">
<template #header><slot name="title"></slot></template>
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
<div class="_content _table">
<div class="_row">
<div class="_cell"><div class="_label">Process</div>{{ activeSincePrevTick | number }}</div>
<div class="_cell"><div class="_label">Active</div>{{ active | number }}</div>
<div class="_cell"><div class="_label">Waiting</div>{{ waiting | number }}</div>
<div class="_cell"><div class="_label">Delayed</div>{{ delayed | number }}</div>
</div>
</div>
<div class="_content" style="margin-bottom: -8px;">
<canvas ref="chart"></canvas>
</div>
</mk-container>
</template>
<script lang="ts">
import Vue from 'vue';
import Chart from 'chart.js';
import { faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../../components/ui/container.vue';
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 Vue.extend({
components: {
MkContainer,
},
props: {
domain: {
required: true
},
connection: {
required: true
},
},
data() {
return {
chart: null,
activeSincePrevTick: 0,
active: 0,
waiting: 0,
delayed: 0,
paused: false,
faPlay, faPause
}
},
mounted() {
// TODO: var(--panel)の色が暗いか明るいかで判定する
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chart = new Chart(this.$refs.chart, {
type: 'bar',
data: {
labels: [],
datasets: [{
label: 'Process',
pointRadius: 0,
lineTension: 0,
borderWidth: 0,
backgroundColor: '#8BC34A',
data: []
}, {
label: 'Active',
pointRadius: 0,
lineTension: 0,
borderWidth: 0,
backgroundColor: '#03A9F4',
data: []
}, {
label: 'Waiting',
pointRadius: 0,
lineTension: 0,
borderWidth: 0,
backgroundColor: '#FFC107',
data: []
}, {
label: 'Delayed',
order: -1,
type: 'line',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#F44336',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
stacked: true,
gridLines: {
display: false,
color: gridColor,
zeroLineColor: gridColor,
},
ticks: {
display: false
}
}],
yAxes: [{
stacked: true,
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);
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
},
methods: {
onStats(stats) {
if (this.paused) return;
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 > 100) {
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);
}
},
pause() {
this.paused = true;
},
resume() {
this.paused = false;
},
}
});
</script>

View File

@@ -1,219 +1,77 @@
<template>
<div v-if="meta" class="xhexznfu" v-size="{ min: [1600] }">
<portal to="icon"><fa :icon="faServer"/></portal>
<portal to="title">{{ $t('instance') }}</portal>
<mk-folder>
<template #header><fa :icon="faTachometerAlt"/> {{ $t('overview') }}</template>
<div v-if="meta" v-show="page === 'index'" class="xhexznfu _section">
<MkFolder>
<template #header><Fa :icon="faTachometerAlt"/> {{ $t('overview') }}</template>
<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }">
<mk-instance-stats :chart-limit="300" :detailed="true" class="stats" ref="stats"/>
<MkInstanceStats :chart-limit="300" :detailed="true" class="_vMargin" ref="stats"/>
<div class="column">
<mk-container :body-togglable="true" :resize-base-el="() => $el" class="info">
<template #header><fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template>
<div class="_content">
<div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
</div>
<div class="_content" v-if="serverInfo">
<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
</div>
</mk-container>
<mk-container :body-togglable="true" :scrollable="true" :resize-base-el="() => $el" class="db">
<template #header><fa :icon="faDatabase"/>{{ $t('database') }}</template>
<div class="_content" v-if="dbInfo">
<table style="border-collapse: collapse; width: 100%;">
<tr style="opacity: 0.7;">
<th style="text-align: left; padding: 0 8px 8px 0;">Table</th>
<th style="text-align: left; padding: 0 8px 8px 0;">Records</th>
<th style="text-align: left; padding: 0 0 8px 0;">Size</th>
</tr>
<tr v-for="table in dbInfo" :key="table[0]">
<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th>
<td style="padding: 0 8px 0 0;">{{ table[1].count | number }}</td>
<td style="padding: 0; opacity: 0.7;">{{ table[1].size | bytes }}</td>
</tr>
</table>
</div>
</mk-container>
<mkw-federation class="fed" :body-togglable="true" :scrollable="true"/>
</div>
</div>
</mk-folder>
<mk-folder style="margin: var(--margin) 0;">
<template #header><fa :icon="faHeartbeat"/> {{ $t('metrics') }}</template>
<div class="segusily">
<mk-container :body-togglable="false" :resize-base-el="() => $el">
<template #header><fa :icon="faMicrochip"/>{{ $t('cpuAndMemory') }}</template>
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="cpumem"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="_table">
<!--
<div class="_row">
<div class="_cell"><div class="_label">CPU</div>{{ serverInfo.cpu.model }}</div>
</div>
-->
<div class="_row">
<div class="_cell"><div class="_label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
<div class="_cell"><div class="_label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
<div class="_cell"><div class="_label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</mk-container>
<mk-container :body-togglable="false" :resize-base-el="() => $el">
<template #header><fa :icon="faHdd"/> {{ $t('disk') }}</template>
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="disk"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="_table">
<div class="_row">
<div class="_cell"><div class="_label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
<div class="_cell"><div class="_label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
<div class="_cell"><div class="_label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</mk-container>
<mk-container :body-togglable="false" :resize-base-el="() => $el">
<template #header><fa :icon="faExchangeAlt"/> {{ $t('network') }}</template>
<template #func><button class="_button" @click="resume" :disabled="!paused"><fa :icon="faPlay"/></button><button class="_button" @click="pause" :disabled="paused"><fa :icon="faPause"/></button></template>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="net"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="_table">
<div class="_row">
<div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
</div>
</div>
</div>
</mk-container>
</div>
</mk-folder>
<mk-folder>
<template #header><fa :icon="faClipboardList"/> {{ $t('jobQueue') }}</template>
<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }">
<mk-container :body-togglable="false" :scrollable="true" :resize-base-el="() => $el">
<template #header><fa :icon="faExclamationTriangle"/> {{ $t('delayed') }}</template>
<MkContainer :body-togglable="true" class="_vMargin">
<template #header><Fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template>
<div class="_content">
<div class="_keyValue" v-for="job in jobs" :key="job[0]">
<button class="_button" @click="showInstanceInfo(job[0])">{{ job[0] }}</button>
<div style="text-align: right;">{{ job[1] | number }} jobs</div>
</div>
<div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
</div>
</mk-container>
<x-queue :connection="queueConnection" domain="inbox" ref="queue" class="queue">
<template #title><fa :icon="faExchangeAlt"/> In</template>
</x-queue>
<x-queue :connection="queueConnection" domain="deliver" class="queue">
<template #title><fa :icon="faExchangeAlt"/> Out</template>
</x-queue>
<div class="_content" v-if="serverInfo">
<div class="_keyValue"><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
<div class="_keyValue"><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
<div class="_keyValue"><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
</div>
</MkContainer>
<MkContainer :body-togglable="true" :scrollable="true" class="_vMargin" style="height: 300px;">
<template #header><Fa :icon="faDatabase"/>{{ $t('database') }}</template>
<div class="_content" v-if="dbInfo">
<table style="border-collapse: collapse; width: 100%;">
<tr style="opacity: 0.7;">
<th style="text-align: left; padding: 0 8px 8px 0;">Table</th>
<th style="text-align: left; padding: 0 8px 8px 0;">Records</th>
<th style="text-align: left; padding: 0 0 8px 0;">Size</th>
</tr>
<tr v-for="table in dbInfo" :key="table[0]">
<th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th>
<td style="padding: 0 8px 0 0;">{{ number(table[1].count) }}</td>
<td style="padding: 0; opacity: 0.7;">{{ bytes(table[1].size) }}</td>
</tr>
</table>
</div>
</MkContainer>
</div>
</mk-folder>
</MkFolder>
</div>
<div v-if="page === 'logs'" class="_section">
<MkFolder>
<template #header><Fa :icon="faStream"/> {{ $t('logs') }}</template>
<mk-folder>
<template #header><fa :icon="faStream"/> {{ $t('logs') }}</template>
<div class="uwuemslx">
<mk-container :body-togglable="false" :resize-base-el="() => $el">
<template #header><fa :icon="faInfoCircle"/>{{ $t('') }}</template>
<div class="_content">
<div class="_keyValue" v-for="log in modLogs">
<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><mk-time :time="log.createdAt" style="opacity: 0.7;"/>
</div>
</div>
</mk-container>
<section class="_card logs">
<div class="_title"><fa :icon="faStream"/> {{ $t('serverLogs') }}</div>
<div class="_content">
<div class="_inputs">
<mk-input v-model="logDomain" :debounce="true">
<span>{{ $t('domain') }}</span>
</mk-input>
<mk-select v-model="logLevel">
<template #label>{{ $t('level') }}</template>
<option value="all">{{ $t('levels.all') }}</option>
<option value="info">{{ $t('levels.info') }}</option>
<option value="success">{{ $t('levels.success') }}</option>
<option value="warning">{{ $t('levels.warning') }}</option>
<option value="error">{{ $t('levels.error') }}</option>
<option value="debug">{{ $t('levels.debug') }}</option>
</mk-select>
</div>
<div class="logs">
<code v-for="log in logs" :key="log.id" :class="log.level">
<details>
<summary><mk-time :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>
</div>
<div class="_footer">
<mk-button @click="deleteAllLogs()" primary><fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</mk-button>
</div>
</section>
<div class="_keyValue" v-for="log in modLogs">
<b>{{ log.type }}</b><span>by {{ log.user.username }}</span><MkTime :time="log.createdAt" style="opacity: 0.7;"/>
</div>
</mk-folder>
</MkFolder>
</div>
<div v-if="page === 'metrics'">
<XMetrics/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { computed, defineComponent, markRaw } from 'vue';
import { faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
import VueJsonPretty from 'vue-json-pretty';
import MkInstanceStats from '../../components/instance-stats.vue';
import MkButton from '../../components/ui/button.vue';
import MkSelect from '../../components/ui/select.vue';
import MkInput from '../../components/ui/input.vue';
import MkContainer from '../../components/ui/container.vue';
import MkFolder from '../../components/ui/folder.vue';
import MkwFederation from '../../widgets/federation.vue';
import { version, url } from '../../config';
import XQueue from './index.queue-chart.vue';
import MkInstanceStats from '@/components/instance-stats.vue';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/ui/select.vue';
import MkInput from '@/components/ui/input.vue';
import MkContainer from '@/components/ui/container.vue';
import MkFolder from '@/components/ui/folder.vue';
import { version, url } from '@/config';
import bytes from '../../filters/bytes';
import number from '../../filters/number';
import MkInstanceInfo from './instance.vue';
import XMetrics from './index.metrics.vue';
import * as os from '@/os';
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 Vue.extend({
metaInfo() {
return {
title: this.$t('instance') as string
};
},
export default defineComponent({
components: {
MkInstanceStats,
MkButton,
@@ -221,31 +79,43 @@ export default Vue.extend({
MkInput,
MkContainer,
MkFolder,
MkwFederation,
XQueue,
XMetrics,
VueJsonPretty,
},
data() {
return {
INFO: {
header: [{
id: 'index',
title: null,
tooltip: this.$t('instance'),
icon: faServer,
onClick: () => { this.page = 'index'; },
selected: computed(() => this.page === 'index')
}, {
id: 'metrics',
title: null,
tooltip: this.$t('metrics'),
icon: faHeartbeat,
onClick: () => { this.page = 'metrics'; },
selected: computed(() => this.page === 'metrics')
}, {
id: 'logs',
title: null,
tooltip: this.$t('logs'),
icon: faStream,
onClick: () => { this.page = 'logs'; },
selected: computed(() => this.page === 'logs')
}]
},
page: 'index',
version,
url,
stats: null,
serverInfo: null,
connection: null,
queueConnection: this.$root.stream.useSharedConnection('queueStats'),
memUsage: 0,
chartCpuMem: null,
chartNet: null,
jobs: [],
logs: [],
logLevel: 'all',
logDomain: '',
modLogs: [],
dbInfo: null,
overviewHeight: '1fr',
queueHeight: '1fr',
paused: false,
faPlay, faPause, faDatabase, faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt, faInfoCircle, faExclamationTriangle, faTachometerAlt, faHeartbeat, faClipboardList,
}
},
@@ -256,509 +126,47 @@ export default Vue.extend({
},
},
watch: {
logLevel() {
this.logs = [];
this.fetchLogs();
},
logDomain() {
this.logs = [];
this.fetchLogs();
}
},
created() {
this.$store.commit('setFullView', true);
},
mounted() {
this.fetchLogs();
this.fetchJobs();
this.fetchModLogs();
// TODO: var(--panel)の色が暗いか明るいかで判定する
const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chartCpuMem = new Chart(this.$refs.cpumem, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#86b300',
backgroundColor: alpha('#86b300', 0.1),
data: []
}, {
label: 'MEM (active)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
data: []
}, {
label: 'MEM (used)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
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,
max: 100
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.chartNet = new Chart(this.$refs.net, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'In',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Out',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
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.chartDisk = new Chart(this.$refs.disk, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Read',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Write',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
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.$root.api('admin/server-info', {}).then(res => {
os.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
this.connection = this.$root.stream.useSharedConnection('serverStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 150
});
this.$nextTick(() => {
this.queueConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
});
});
this.$root.api('admin/get-table-stats', {}).then(res => {
os.api('admin/get-table-stats', {}).then(res => {
this.dbInfo = Object.entries(res).sort((a, b) => b[1].size - a[1].size);
});
this.$nextTick(() => {
new ResizeObserver((entries, observer) => {
if (this.$refs.stats && this.$refs.stats.$el) {
this.overviewHeight = this.$refs.stats.$el.offsetHeight + 'px';
}
}).observe(this.$refs.stats.$el);
new ResizeObserver((entries, observer) => {
if (this.$refs.queue && this.$refs.queue.$el) {
this.queueHeight = this.$refs.queue.$el.offsetHeight + 'px';
}
}).observe(this.$refs.queue.$el);
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
this.connection.dispose();
this.queueConnection.dispose();
this.$store.commit('setFullView', false);
},
methods: {
async showInstanceInfo(q) {
let instance = q;
if (typeof q === 'string') {
instance = await this.$root.api('federation/show-instance', {
instance = await os.api('federation/show-instance', {
host: q
});
}
this.$root.new(MkInstanceInfo, {
os.popup(MkInstanceInfo, {
instance: instance
});
},
fetchLogs() {
this.$root.api('admin/logs', {
level: this.logLevel === 'all' ? null : this.logLevel,
domain: this.logDomain === '' ? null : this.logDomain,
limit: 30
}).then(logs => {
this.logs = logs.reverse();
});
}, {}, 'closed');
},
fetchJobs() {
this.$root.api('admin/queue/deliver-delayed', {}).then(jobs => {
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
this.jobs = jobs;
});
},
fetchModLogs() {
this.$root.api('admin/show-moderation-logs', {}).then(logs => {
os.api('admin/show-moderation-logs', {}).then(logs => {
this.modLogs = logs;
});
},
deleteAllLogs() {
this.$root.api('admin/delete-logs').then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
bytes,
onStats(stats) {
if (this.paused) return;
const cpu = (stats.cpu * 100).toFixed(0);
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
this.memUsage = stats.mem.active;
this.chartCpuMem.data.labels.push('');
this.chartCpuMem.data.datasets[0].data.push(cpu);
this.chartCpuMem.data.datasets[1].data.push(memActive);
this.chartCpuMem.data.datasets[2].data.push(memUsed);
this.chartNet.data.labels.push('');
this.chartNet.data.datasets[0].data.push(stats.net.rx);
this.chartNet.data.datasets[1].data.push(stats.net.tx);
this.chartDisk.data.labels.push('');
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
this.chartCpuMem.data.labels.shift();
this.chartCpuMem.data.datasets[0].data.shift();
this.chartCpuMem.data.datasets[1].data.shift();
this.chartCpuMem.data.datasets[2].data.shift();
this.chartNet.data.labels.shift();
this.chartNet.data.datasets[0].data.shift();
this.chartNet.data.datasets[1].data.shift();
this.chartDisk.data.labels.shift();
this.chartDisk.data.datasets[0].data.shift();
this.chartDisk.data.datasets[1].data.shift();
}
this.chartCpuMem.update();
this.chartNet.update();
this.chartDisk.update();
},
onStatsLog(statsLog) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},
pause() {
this.paused = true;
},
resume() {
this.paused = false;
},
number,
}
});
</script>
<style lang="scss" scoped>
.xhexznfu {
&.min-width_1600px {
.sboqnrfi {
display: grid;
grid-template-columns: 3.2fr 1fr;
grid-template-rows: 1fr;
gap: 16px 16px;
> .stats {
height: min-content;
}
> .column {
display: flex;
flex-direction: column;
> .info {
flex-shrink: 0;
flex-grow: 0;
}
> .db {
flex: 1;
flex-grow: 0;
height: 100%;
}
> .fed {
flex: 1;
flex-grow: 0;
height: 100%;
}
> *:not(:last-child) {
margin-bottom: var(--margin);
}
}
}
.segusily {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr;
gap: 16px 16px;
}
.vkyrmkwb {
display: grid;
grid-template-columns: 0.5fr 1fr 1fr;
grid-template-rows: 1fr;
gap: 16px 16px;
margin-bottom: var(--margin);
> .queue {
height: min-content;
}
> * {
margin-bottom: 0;
}
}
.uwuemslx {
display: grid;
grid-template-columns: 2fr 3fr;
grid-template-rows: 1fr;
gap: 16px 16px;
height: 400px;
}
}
.vkyrmkwb {
> * {
margin-bottom: var(--margin);
}
}
> .stats {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin: calc(0px - var(--margin) / 2);
margin-bottom: calc(var(--margin) / 2);
> div {
flex: 1 0 213px;
margin: calc(var(--margin) / 2);
box-sizing: border-box;
padding: 16px;
}
}
> .logs {
> ._content {
> .logs {
padding: 8px;
background: #000;
color: #fff;
font-size: 0.9em;
> code {
display: block;
&.error {
color: #f00;
}
&.warning {
color: #ff0;
}
&.success {
color: #0f0;
}
&.debug {
opacity: 0.7;
}
}
}
}
}
}
</style>

View File

@@ -1,8 +1,13 @@
<template>
<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true" :width="520" :height="500">
<XModalWindow ref="dialog"
:width="520"
:height="500"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template #header>{{ instance.host }}</template>
<div class="mk-instance-info">
<div class="_table">
<div class="_table section">
<div class="_row">
<div class="_cell">
<div class="_label">{{ $t('software') }}</div>
@@ -14,47 +19,47 @@
</div>
</div>
</div>
<div class="_table data">
<div class="_table data section">
<div class="_row">
<div class="_cell">
<div class="_label">{{ $t('registeredAt') }}</div>
<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<mk-time :time="instance.caughtAt"/>)</div>
<div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div>
</div>
</div>
<div class="_row">
<div class="_cell">
<div class="_label">{{ $t('following') }}</div>
<button class="_data _textButton" @click="showFollowing()">{{ instance.followingCount | number }}</button>
<button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
</div>
<div class="_cell">
<div class="_label">{{ $t('followers') }}</div>
<button class="_data _textButton" @click="showFollowers()">{{ instance.followersCount | number }}</button>
<button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
</div>
</div>
<div class="_row">
<div class="_cell">
<div class="_label">{{ $t('users') }}</div>
<button class="_data _textButton" @click="showUsers()">{{ instance.usersCount | number }}</button>
<button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
</div>
<div class="_cell">
<div class="_label">{{ $t('notes') }}</div>
<div class="_data">{{ instance.notesCount | number }}</div>
<div class="_data">{{ number(instance.notesCount) }}</div>
</div>
</div>
<div class="_row">
<div class="_cell">
<div class="_label">{{ $t('files') }}</div>
<div class="_data">{{ instance.driveFiles | number }}</div>
<div class="_data">{{ number(instance.driveFiles) }}</div>
</div>
<div class="_cell">
<div class="_label">{{ $t('storageUsage') }}</div>
<div class="_data">{{ instance.driveUsage | bytes }}</div>
<div class="_data">{{ bytes(instance.driveUsage) }}</div>
</div>
</div>
<div class="_row">
<div class="_cell">
<div class="_label">{{ $t('latestRequestSentAt') }}</div>
<div class="_data"><mk-time v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
<div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
</div>
<div class="_cell">
<div class="_label">{{ $t('latestStatus') }}</div>
@@ -64,7 +69,7 @@
<div class="_row">
<div class="_cell">
<div class="_label">{{ $t('latestRequestReceivedAt') }}</div>
<div class="_data"><mk-time v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
<div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
</div>
</div>
</div>
@@ -72,7 +77,7 @@
<div class="header">
<span class="label">{{ $t('charts') }}</span>
<div class="selects">
<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
<option value="requests">{{ $t('_instanceCharts.requests') }}</option>
<option value="users">{{ $t('_instanceCharts.users') }}</option>
<option value="users-total">{{ $t('_instanceCharts.usersTotal') }}</option>
@@ -84,49 +89,52 @@
<option value="drive-usage-total">{{ $t('_instanceCharts.cacheSizeTotal') }}</option>
<option value="drive-files">{{ $t('_instanceCharts.files') }}</option>
<option value="drive-files-total">{{ $t('_instanceCharts.filesTotal') }}</option>
</mk-select>
<mk-select v-model="chartSpan" style="margin: 0;">
</MkSelect>
<MkSelect v-model:value="chartSpan" style="margin: 0;">
<option value="hour">{{ $t('perHour') }}</option>
<option value="day">{{ $t('perDay') }}</option>
</mk-select>
</MkSelect>
</div>
</div>
<div class="chart">
<canvas ref="chart"></canvas>
<canvas :ref="setChart"></canvas>
</div>
</div>
<div class="operations">
<div class="operations section">
<span class="label">{{ $t('operations') }}</span>
<mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch>
<mk-switch :value="isBlocked" class="switch" @change="changeBlock">{{ $t('blockThisInstance') }}</mk-switch>
<MkSwitch v-model:value="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</MkSwitch>
<MkSwitch :value="isBlocked" class="switch" @update:value="changeBlock">{{ $t('blockThisInstance') }}</MkSwitch>
<details>
<summary>{{ $t('deleteAllFiles') }}</summary>
<mk-button @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</mk-button>
<MkButton @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><Fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</MkButton>
</details>
<details>
<summary>{{ $t('removeAllFollowing') }}</summary>
<mk-button @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><fa :icon="faMinusCircle"/> {{ $t('removeAllFollowing') }}</mk-button>
<mk-info warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</mk-info>
<MkButton @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><Fa :icon="faMinusCircle"/> {{ $t('removeAllFollowing') }}</MkButton>
<MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo>
</details>
</div>
<details class="metadata">
<details class="metadata section">
<summary class="label">{{ $t('metadata') }}</summary>
<pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre>
</details>
</div>
</x-window>
</XModalWindow>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import Chart from 'chart.js';
import { faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown, faMinusCircle, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import XWindow from '../../components/window.vue';
import MkUsersDialog from '../../components/users-dialog.vue';
import MkSelect from '../../components/ui/select.vue';
import MkButton from '../../components/ui/button.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkUsersDialog from '@/components/users-dialog.vue';
import MkSelect from '@/components/ui/select.vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkInfo from '@/components/ui/info.vue';
import bytes from '../../filters/bytes';
import number from '../../filters/number';
import * as os from '@/os';
const chartLimit = 90;
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
@@ -139,9 +147,9 @@ const alpha = hex => {
return `rgba(${r}, ${g}, ${b}, 0.1)`;
};
export default Vue.extend({
export default defineComponent({
components: {
XWindow,
XModalWindow,
MkSelect,
MkButton,
MkSwitch,
@@ -155,10 +163,13 @@ export default Vue.extend({
}
},
emits: ['closed'],
data() {
return {
isSuspended: this.instance.isSuspended,
now: null,
canvas: null,
chart: null,
chartInstance: null,
chartSrc: 'requests',
@@ -199,13 +210,13 @@ export default Vue.extend({
},
isBlocked() {
return this.meta && this.meta.blockedHosts.includes(this.instance.host);
return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host);
}
},
watch: {
isSuspended() {
this.$root.api('admin/federation/update-instance', {
os.api('admin/federation/update-instance', {
host: this.instance.host,
isSuspended: this.isSuspended
});
@@ -220,12 +231,12 @@ export default Vue.extend({
}
},
async created() {
async created() {
this.now = new Date();
const [perHour, perDay] = await Promise.all([
this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
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 = {
@@ -239,8 +250,12 @@ export default Vue.extend({
},
methods: {
setChart(el) {
this.canvas = el;
},
changeBlock(e) {
this.$root.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)
});
},
@@ -250,24 +265,14 @@ export default Vue.extend({
},
removeAllFollowing() {
this.$root.api('admin/federation/remove-all-following', {
os.apiWithDialog('admin/federation/remove-all-following', {
host: this.instance.host
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
deleteAllFiles() {
this.$root.api('admin/federation/delete-all-files', {
os.apiWithDialog('admin/federation/delete-all-files', {
host: this.instance.host
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
@@ -277,7 +282,7 @@ export default Vue.extend({
}
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chartInstance = new Chart(this.$refs.chart, {
this.chartInstance = new Chart(this.canvas, {
type: 'line',
data: {
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
@@ -436,7 +441,7 @@ export default Vue.extend({
},
showFollowing() {
this.$root.new(MkUsersDialog, {
os.modal(MkUsersDialog, {
title: this.$t('instanceFollowing'),
pagination: {
endpoint: 'federation/following',
@@ -450,7 +455,7 @@ export default Vue.extend({
},
showFollowers() {
this.$root.new(MkUsersDialog, {
os.modal(MkUsersDialog, {
title: this.$t('instanceFollowers'),
pagination: {
endpoint: 'federation/followers',
@@ -464,7 +469,7 @@ export default Vue.extend({
},
showUsers() {
this.$root.new(MkUsersDialog, {
os.modal(MkUsersDialog, {
title: this.$t('instanceUsers'),
pagination: {
endpoint: 'federation/users',
@@ -474,7 +479,11 @@ export default Vue.extend({
}
}
});
}
},
bytes,
number
}
});
</script>
@@ -483,34 +492,21 @@ export default Vue.extend({
.mk-instance-info {
overflow: auto;
> ._table {
padding: 0 32px;
> .section {
padding: 16px 32px;
@media (max-width: 500px) {
padding: 0 16px;
padding: 8px 16px;
}
}
> .data {
margin-top: 16px;
padding-top: 16px;
border-top: solid 1px var(--divider);
@media (max-width: 500px) {
margin-top: 8px;
padding-top: 8px;
&:not(:first-child) {
border-top: solid 1px var(--divider);
}
}
> .chart {
margin-top: 16px;
padding-top: 16px;
border-top: solid 1px var(--divider);
@media (max-width: 500px) {
margin-top: 8px;
padding-top: 8px;
}
padding: 16px 0 12px 0;
> .header {
padding: 0 32px;
@@ -539,15 +535,6 @@ export default Vue.extend({
}
> .operations {
padding: 16px 32px 16px 32px;
margin-top: 8px;
border-top: solid 1px var(--divider);
@media (max-width: 500px) {
padding: 8px 16px 8px 16px;
margin-top: 0;
}
> .label {
font-size: 80%;
opacity: 0.7;
@@ -559,13 +546,6 @@ export default Vue.extend({
}
> .metadata {
padding: 16px 32px 16px 32px;
border-top: solid 1px var(--divider);
@media (max-width: 500px) {
padding: 8px 16px 8px 16px;
}
> .label {
font-size: 80%;
opacity: 0.7;

View File

@@ -0,0 +1,95 @@
<template>
<div class="_section">
<div class="_inputs">
<MkInput v-model:value="logDomain" :debounce="true">
<span>{{ $t('domain') }}</span>
</MkInput>
<MkSelect v-model:value="logLevel">
<template #label>{{ $t('level') }}</template>
<option value="all">{{ $t('levels.all') }}</option>
<option value="info">{{ $t('levels.info') }}</option>
<option value="success">{{ $t('levels.success') }}</option>
<option value="warning">{{ $t('levels.warning') }}</option>
<option value="error">{{ $t('levels.error') }}</option>
<option value="debug">{{ $t('levels.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><Fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faStream } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkSelect from '@/components/ui/select.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkInput,
MkSelect,
MkTextarea,
},
data() {
return {
INFO: {
header: [{
title: this.$t('serverLogs'),
icon: faStream
}]
},
logs: [],
logLevel: 'all',
logDomain: '',
faTrashAlt,
}
},
watch: {
logLevel() {
this.logs = [];
this.fetchLogs();
},
logDomain() {
this.logs = [];
this.fetchLogs();
}
},
created() {
this.fetchLogs();
},
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,12 +1,12 @@
<template>
<section class="_card">
<section class="_section">
<div class="_title"><slot name="title"></slot></div>
<div class="_content _table">
<div class="_row">
<div class="_cell"><div class="_label">Process</div>{{ activeSincePrevTick | number }}</div>
<div class="_cell"><div class="_label">Active</div>{{ active | number }}</div>
<div class="_cell"><div class="_label">Waiting</div>{{ waiting | number }}</div>
<div class="_cell"><div class="_label">Delayed</div>{{ delayed | number }}</div>
<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="_content" style="margin-bottom: -8px;">
@@ -16,7 +16,7 @@
<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;">({{ job[1] | number }} jobs)</span>
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
</div>
</div>
<span v-else style="opacity: 0.5;">{{ $t('noJobs') }}</span>
@@ -25,8 +25,9 @@
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import Chart from 'chart.js';
import number from '../../filters/number';
const alpha = (hex, a) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
@@ -35,8 +36,9 @@ const alpha = (hex, a) => {
const b = parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
props: {
domain: {
required: true
@@ -154,7 +156,7 @@ export default Vue.extend({
this.connection.on('statsLog', this.onStatsLog);
},
beforeDestroy() {
beforeUnmount() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
},
@@ -187,10 +189,12 @@ export default Vue.extend({
},
fetchJobs() {
this.$root.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
this.jobs = jobs;
});
},
number
}
});
</script>

View File

@@ -1,36 +1,28 @@
<template>
<div>
<portal to="icon"><fa :icon="faExchangeAlt"/></portal>
<portal to="title">{{ $t('jobQueue') }}</portal>
<x-queue :connection="connection" domain="inbox">
<template #title><fa :icon="faExchangeAlt"/> In</template>
</x-queue>
<x-queue :connection="connection" domain="deliver">
<template #title><fa :icon="faExchangeAlt"/> Out</template>
</x-queue>
<section class="_card">
<XQueue :connection="connection" domain="inbox">
<template #title><Fa :icon="faExchangeAlt"/> In</template>
</XQueue>
<XQueue :connection="connection" domain="deliver">
<template #title><Fa :icon="faExchangeAlt"/> Out</template>
</XQueue>
<section class="_section">
<div class="_content">
<mk-button @click="clear()"><fa :icon="faTrashAlt"/> {{ $t('clearQueue') }}</mk-button>
<MkButton @click="clear()"><Fa :icon="faTrashAlt"/> {{ $t('clearQueue') }}</MkButton>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.vue';
import * as os from '@/os';
export default Vue.extend({
metaInfo() {
return {
title: `${this.$t('jobQueue')} | ${this.$t('instance')}`
};
},
export default defineComponent({
components: {
MkButton,
XQueue,
@@ -38,7 +30,13 @@ export default Vue.extend({
data() {
return {
connection: this.$root.stream.useSharedConnection('queueStats'),
INFO: {
header: [{
title: this.$t('jobQueue'),
icon: faExchangeAlt,
}],
},
connection: os.stream.useSharedConnection('queueStats'),
faExchangeAlt, faTrashAlt
}
},
@@ -52,13 +50,13 @@ export default Vue.extend({
});
},
beforeDestroy() {
beforeUnmount() {
this.connection.dispose();
},
methods: {
clear() {
this.$root.dialog({
os.dialog({
type: 'warning',
title: this.$t('clearQueueConfirmTitle'),
text: this.$t('clearQueueConfirmText'),
@@ -66,12 +64,7 @@ export default Vue.extend({
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('admin/queue/clear', {}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
os.apiWithDialog('admin/queue/clear', {});
});
}
}

View File

@@ -1,43 +1,35 @@
<template>
<div class="relaycxt">
<portal to="icon"><fa :icon="faProjectDiagram"/></portal>
<portal to="title">{{ $t('relays') }}</portal>
<section class="_card _vMargin add">
<div class="_title"><fa :icon="faPlus"/> {{ $t('addRelay') }}</div>
<section class="_section add">
<div class="_title"><Fa :icon="faPlus"/> {{ $t('addRelay') }}</div>
<div class="_content">
<mk-input v-model="inbox">
<MkInput v-model:value="inbox">
<span>{{ $t('inboxUrl') }}</span>
</mk-input>
<mk-button @click="add(inbox)" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
</MkInput>
<MkButton @click="add(inbox)" primary><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton>
</div>
</section>
<section class="_card _vMargin relays">
<div class="_title"><fa :icon="faProjectDiagram"/> {{ $t('addedRelays') }}</div>
<section class="_section relays">
<div class="_title"><Fa :icon="faProjectDiagram"/> {{ $t('addedRelays') }}</div>
<div class="_content relay" v-for="relay in relays" :key="relay.inbox">
<div>{{ relay.inbox }}</div>
<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
<mk-button class="button" inline @click="remove(relay.inbox)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button>
<MkButton class="button" inline @click="remove(relay.inbox)"><Fa :icon="faTrashAlt"/> {{ $t('remove') }}</MkButton>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faPlus, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('relays') as string
};
},
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkInput,
@@ -45,6 +37,12 @@ export default Vue.extend({
data() {
return {
INFO: {
header: [{
title: this.$t('relays'),
icon: faProjectDiagram,
}],
},
relays: [],
inbox: '',
faPlus, faProjectDiagram, faSave, faTrashAlt
@@ -57,12 +55,12 @@ export default Vue.extend({
methods: {
add(inbox: string) {
this.$root.api('admin/relays/add', {
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
this.refresh();
}).catch((e: any) => {
this.$root.dialog({
os.dialog({
type: 'error',
text: e.message || e
});
@@ -70,12 +68,12 @@ export default Vue.extend({
},
remove(inbox: string) {
this.$root.api('admin/relays/remove', {
os.api('admin/relays/remove', {
inbox
}).then(() => {
this.refresh();
}).catch((e: any) => {
this.$root.dialog({
os.dialog({
type: 'error',
text: e.message || e
});
@@ -83,7 +81,7 @@ export default Vue.extend({
},
refresh() {
this.$root.api('admin/relays/list').then((relays: any) => {
os.api('admin/relays/list').then((relays: any) => {
this.relays = relays;
});
}

View File

@@ -1,53 +1,50 @@
<template>
<div v-if="meta">
<portal to="icon"><fa :icon="faCog"/></portal>
<portal to="title">{{ $t('settings') }}</portal>
<section class="_card _vMargin info">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<section class="_section info">
<div class="_title"><Fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<div class="_content">
<mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
<MkInput v-model:value="name">{{ $t('instanceName') }}</MkInput>
<MkTextarea v-model:value="description">{{ $t('instanceDescription') }}</MkTextarea>
<MkInput v-model:value="iconUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('iconUrl') }}</MkInput>
<MkInput v-model:value="bannerUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</MkInput>
<MkInput v-model:value="tosUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('tosUrl') }}</MkInput>
<MkInput v-model:value="maintainerName">{{ $t('maintainerName') }}</MkInput>
<MkInput v-model:value="maintainerEmail" type="email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</MkInput>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin info">
<section class="_section info">
<div class="_content">
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
<MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><Fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</MkInput>
</div>
<div class="_content">
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
<MkSwitch v-model:value="enableLocalTimeline" @update:value="save()">{{ $t('enableLocalTimeline') }}</MkSwitch>
<MkSwitch v-model:value="enableGlobalTimeline" @update:value="save()">{{ $t('enableGlobalTimeline') }}</MkSwitch>
<MkInfo>{{ $t('disablingTimelinesInfo') }}</MkInfo>
</div>
<div class="_content">
<mk-switch v-model="useStarForReactionFallback" @change="save()">{{ $t('useStarForReactionFallback') }}</mk-switch>
<MkSwitch v-model:value="useStarForReactionFallback" @update:value="save()">{{ $t('useStarForReactionFallback') }}</MkSwitch>
</div>
</section>
<section class="_card _vMargin info">
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
<section class="_section info">
<div class="_title"><Fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
<MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $t('enableRegistration') }}</MkSwitch>
<MkButton v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
<div class="_content">
<mk-switch v-model="enableHcaptcha" ref="enableHcaptcha">{{ $t('enableHcaptcha') }}</mk-switch>
<MkSwitch v-model:value="enableHcaptcha">{{ $t('enableHcaptcha') }}</MkSwitch>
<template v-if="enableHcaptcha">
<mk-input v-model="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSiteKey') }}</mk-input>
<mk-input v-model="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSecretKey') }}</mk-input>
<MkInput v-model:value="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('hcaptchaSiteKey') }}</MkInput>
<MkInput v-model:value="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('hcaptchaSecretKey') }}</MkInput>
</template>
</div>
<div class="_content" v-if="enableHcaptcha">
@@ -55,17 +52,17 @@
<captcha v-if="enableHcaptcha" provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<mk-switch v-model="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
<MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</MkSwitch>
<template v-if="enableRecaptcha">
<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
<MkInput v-model:value="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</MkInput>
<MkInput v-model:value="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><Fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</MkInput>
</template>
</div>
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
@@ -73,198 +70,198 @@
<captcha v-if="enableRecaptcha" provider="grecaptcha" :sitekey="recaptchaSiteKey"/>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
<div class="_content">
<mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch>
<mk-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</mk-input>
<MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></MkSwitch>
<MkInput v-model:value="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</MkInput>
<div><b>{{ $t('smtpConfig') }}</b></div>
<div class="_inputs">
<mk-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</mk-input>
<mk-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</mk-input>
<MkInput v-model:value="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</MkInput>
<MkInput v-model:value="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</MkInput>
</div>
<div class="_inputs">
<mk-input v-model="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</mk-input>
<mk-input v-model="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</mk-input>
<MkInput v-model:value="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</MkInput>
<MkInput v-model:value="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</MkInput>
</div>
<mk-info>{{ $t('emptyToDisableSmtpAuth') }}</mk-info>
<mk-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></mk-switch>
<MkInfo>{{ $t('emptyToDisableSmtpAuth') }}</MkInfo>
<MkSwitch v-model:value="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></MkSwitch>
<div>
<mk-button :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</mk-button>
<mk-button :disabled="!enableEmail" primary inline @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</MkButton>
<MkButton :disabled="!enableEmail" primary inline @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
<MkSwitch v-model:value="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></MkSwitch>
<template v-if="enableServiceWorker">
<div class="_inputs">
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
<MkInput v-model:value="swPublicKey" :disabled="!enableServiceWorker"><template #icon><Fa :icon="faKey"/></template>Public key</MkInput>
<MkInput v-model:value="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><Fa :icon="faKey"/></template>Private key</MkInput>
</div>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<mk-textarea v-model="pinnedUsers">
<MkTextarea v-model:value="pinnedUsers">
<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
</mk-textarea>
</MkTextarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
<MkSwitch v-model:value="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></MkSwitch>
<MkSwitch v-model:value="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></MkSwitch>
<MkInput v-model:value="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></MkInput>
<MkInput v-model:value="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></MkInput>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
<div class="_content">
<mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch>
<MkSwitch v-model:value="useObjectStorage">{{ $t('useObjectStorage') }}</MkSwitch>
<template v-if="useObjectStorage">
<mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('objectStorageBaseUrl') }}<template #desc>{{ $t('objectStorageBaseUrlDesc') }}</template></mk-input>
<MkInput v-model:value="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('objectStorageBaseUrl') }}<template #desc>{{ $t('objectStorageBaseUrlDesc') }}</template></MkInput>
<div class="_inputs">
<mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('objectStorageBucket') }}<template #desc>{{ $t('objectStorageBucketDesc') }}</template></mk-input>
<mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('objectStoragePrefix') }}<template #desc>{{ $t('objectStoragePrefixDesc') }}</template></mk-input>
<MkInput v-model:value="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('objectStorageBucket') }}<template #desc>{{ $t('objectStorageBucketDesc') }}</template></MkInput>
<MkInput v-model:value="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('objectStoragePrefix') }}<template #desc>{{ $t('objectStoragePrefixDesc') }}</template></MkInput>
</div>
<mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('objectStorageEndpoint') }}<template #desc>{{ $t('objectStorageEndpointDesc') }}</template></mk-input>
<MkInput v-model:value="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('objectStorageEndpoint') }}<template #desc>{{ $t('objectStorageEndpointDesc') }}</template></MkInput>
<div class="_inputs">
<mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('objectStorageRegion') }}<template #desc>{{ $t('objectStorageRegionDesc') }}</template></mk-input>
<MkInput v-model:value="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('objectStorageRegion') }}<template #desc>{{ $t('objectStorageRegionDesc') }}</template></MkInput>
</div>
<div class="_inputs">
<mk-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Access key</mk-input>
<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input>
<MkInput v-model:value="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><Fa :icon="faKey"/></template>Access key</MkInput>
<MkInput v-model:value="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><Fa :icon="faKey"/></template>Secret key</MkInput>
</div>
<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></mk-switch>
<mk-switch v-model="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $t('objectStorageUseProxy') }}<template #desc>{{ $t('objectStorageUseProxyDesc') }}</template></mk-switch>
<mk-switch v-model="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $t('objectStorageSetPublicRead') }}</mk-switch>
<MkSwitch v-model:value="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></MkSwitch>
<MkSwitch v-model:value="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $t('objectStorageUseProxy') }}<template #desc>{{ $t('objectStorageUseProxyDesc') }}</template></MkSwitch>
<MkSwitch v-model:value="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $t('objectStorageSetPublicRead') }}</MkSwitch>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
<MkInput :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></MkInput>
<MkButton primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<mk-textarea v-model="blockedHosts">
<MkTextarea v-model:value="blockedHosts">
<template #desc>{{ $t('blockedInstancesDescription') }}</template>
</mk-textarea>
</MkTextarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<section class="_section">
<div class="_title"><Fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><fa :icon="faTwitter"/> Twitter</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
<header><Fa :icon="faTwitter"/> Twitter</header>
<MkSwitch v-model:value="enableTwitterIntegration">{{ $t('enable') }}</MkSwitch>
<template v-if="enableTwitterIntegration">
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
<MkInfo>Callback URL: {{ `${url}/api/tw/cb` }}</MkInfo>
<MkInput v-model:value="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><Fa :icon="faKey"/></template>Consumer Key</MkInput>
<MkInput v-model:value="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><Fa :icon="faKey"/></template>Consumer Secret</MkInput>
</template>
</div>
<div class="_content">
<header><fa :icon="faGithub"/> GitHub</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
<header><Fa :icon="faGithub"/> GitHub</header>
<MkSwitch v-model:value="enableGithubIntegration">{{ $t('enable') }}</MkSwitch>
<template v-if="enableGithubIntegration">
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
<MkInfo>Callback URL: {{ `${url}/api/gh/cb` }}</MkInfo>
<MkInput v-model:value="githubClientId" :disabled="!enableGithubIntegration"><template #icon><Fa :icon="faKey"/></template>Client ID</MkInput>
<MkInput v-model:value="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><Fa :icon="faKey"/></template>Client Secret</MkInput>
</template>
</div>
<div class="_content">
<header><fa :icon="faDiscord"/> Discord</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
<header><Fa :icon="faDiscord"/> Discord</header>
<MkSwitch v-model:value="enableDiscordIntegration">{{ $t('enable') }}</MkSwitch>
<template v-if="enableDiscordIntegration">
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
<MkInfo>Callback URL: {{ `${url}/api/dc/cb` }}</MkInfo>
<MkInput v-model:value="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><Fa :icon="faKey"/></template>Client ID</MkInput>
<MkInput v-model:value="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><Fa :icon="faKey"/></template>Client Secret</MkInput>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div>
<section class="_section">
<div class="_title"><Fa :icon="faArchway" /> Summaly Proxy</div>
<div class="_content">
<mk-input v-model="summalyProxy">URL</mk-input>
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<MkInput v-model:value="summalyProxy">URL</MkInput>
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent, defineAsyncComponent } from 'vue';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import MkUserSelect from '../../components/user-select.vue';
import { url } from '../../config';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkInfo from '@/components/ui/info.vue';
import { url } from '@/config';
import getAcct from '../../../misc/acct/render';
import * as os from '@/os';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('instance') as string
};
},
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
MkSwitch,
MkInfo,
Captcha: () => import('../../components/captcha.vue').then(x => x.default),
Captcha: defineAsyncComponent(() => import('@/components/captcha.vue')),
},
data() {
return {
INFO: {
header: [{
title: this.$t('instance'),
icon: faCog,
}],
},
url,
proxyAccount: null,
proxyAccountId: null,
@@ -394,16 +391,16 @@ export default Vue.extend({
this.summalyProxy = this.meta.summalyProxy;
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
os.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
this.proxyAccount = proxyAccount;
});
}
},
mounted() {
this.$refs.enableHcaptcha.$on('change', () => {
this.$watch('enableHcaptcha', () => {
if (this.enableHcaptcha && this.enableRecaptcha) {
this.$root.dialog({
os.dialog({
type: 'question', // warning だと間違って cancel するかもしれない
showCancelButton: true,
title: this.$t('settingGuide'),
@@ -418,9 +415,9 @@ export default Vue.extend({
}
});
this.$refs.enableRecaptcha.$on('change', () => {
this.$watch('enableRecaptcha', () => {
if (this.enableRecaptcha && this.enableHcaptcha) {
this.$root.dialog({
os.dialog({
type: 'question', // warning だと間違って cancel するかもしれない
showCancelButton: true,
title: this.$t('settingGuide'),
@@ -438,13 +435,13 @@ export default Vue.extend({
methods: {
invite() {
this.$root.api('admin/invite').then(x => {
this.$root.dialog({
os.api('admin/invite').then(x => {
os.dialog({
type: 'info',
text: x.code
});
}).catch(e => {
this.$root.dialog({
os.dialog({
type: 'error',
text: e
});
@@ -452,7 +449,7 @@ export default Vue.extend({
},
addPinUser() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
os.selectUser().then(user => {
this.pinnedUsers = this.pinnedUsers.trim();
this.pinnedUsers += '\n@' + getAcct(user);
this.pinnedUsers = this.pinnedUsers.trim();
@@ -460,7 +457,7 @@ export default Vue.extend({
},
chooseProxyAccount() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
os.selectUser().then(user => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save(true);
@@ -468,17 +465,17 @@ export default Vue.extend({
},
async testEmail() {
this.$root.api('admin/send-email', {
os.api('admin/send-email', {
to: this.maintainerEmail,
subject: 'Test email',
text: 'Yo'
}).then(x => {
this.$root.dialog({
os.dialog({
type: 'success',
splash: true
});
}).catch(e => {
this.$root.dialog({
os.dialog({
type: 'error',
text: e
});
@@ -486,7 +483,7 @@ export default Vue.extend({
},
save(withDialog = false) {
this.$root.api('admin/update-meta', {
os.api('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
@@ -547,13 +544,10 @@ export default Vue.extend({
}).then(() => {
this.$store.dispatch('instance/fetch');
if (withDialog) {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
os.success();
}
}).catch(e => {
this.$root.dialog({
os.dialog({
type: 'error',
text: e
});

View File

@@ -0,0 +1,233 @@
<template>
<XModalWindow ref="dialog"
:width="370"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
>
<template #header v-if="user"><MkUserName class="name" :user="user"/></template>
<div class="vrcsvlkm" v-if="user && info">
<div class="_section">
<div class="banner" :style="bannerStyle">
<MkAvatar class="avatar" :user="user"/>
</div>
</div>
<div class="_section">
<div class="title">
<span class="acct">@{{ acct(user) }}</span>
</div>
<div class="status">
<span class="staff" v-if="user.isAdmin"><Fa :icon="faBookmark"/></span>
<span class="staff" v-if="user.isModerator"><Fa :icon="farBookmark"/></span>
<span class="punished" v-if="user.isSilenced"><Fa :icon="faMicrophoneSlash"/></span>
<span class="punished" v-if="user.isSuspended"><Fa :icon="faSnowflake"/></span>
</div>
</div>
<div class="_section">
<div class="_content">
<MkSwitch v-if="user.host == null && $store.state.i.isAdmin && (this.moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $t('moderator') }}</MkSwitch>
<MkSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $t('silence') }}</MkSwitch>
<MkSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $t('suspend') }}</MkSwitch>
</div>
</div>
<div class="_section">
<div class="_content">
<MkButton full @click="openProfile"><Fa :icon="faExternalLinkSquareAlt"/> {{ $t('profile') }}</MkButton>
<MkButton full v-if="user.host != null" @click="updateRemoteUser"><Fa :icon="faSync"/> {{ $t('updateRemoteUser') }}</MkButton>
<MkButton full @click="resetPassword"><Fa :icon="faKey"/> {{ $t('resetPassword') }}</MkButton>
<MkButton full @click="deleteAllFiles" danger><Fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</MkButton>
</div>
</div>
<div class="_section">
<details class="_content rawdata">
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
</details>
</div>
</div>
</XModalWindow>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake, faTrashAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import Progress from '@/scripts/loading';
import { acct, userPage } from '../../filters/user';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
XModalWindow,
},
props: {
userId: {
required: true,
}
},
emits: ['closed'],
data() {
return {
user: null,
info: null,
moderator: false,
silenced: false,
suspended: false,
faTimes, faBookmark, farBookmark, faKey, faSync, faMicrophoneSlash, faSnowflake, faTrashAlt, faExternalLinkSquareAlt
};
},
computed: {
bannerStyle(): any {
if (this.user.bannerUrl == null) return {};
return {
backgroundImage: `url(${ this.user.bannerUrl })`
};
},
},
created() {
this.fetch();
},
methods: {
async fetch() {
Progress.start();
this.user = await os.api('users/show', { userId: this.userId });
this.info = await os.api('admin/show-user', { userId: this.userId });
this.moderator = this.info.isModerator;
this.silenced = this.info.isSilenced;
this.suspended = this.info.isSuspended;
Progress.done();
},
/** 処理対象ユーザーの情報を更新する */
async refreshUser() {
this.user = await os.api('users/show', { userId: this.user.id });
this.info = await os.api('admin/show-user', { userId: this.user.id });
},
openProfile() {
window.open(userPage(this.user, null, true), '_blank');
},
async updateRemoteUser() {
await os.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
os.success();
});
await this.refreshUser();
},
async resetPassword() {
os.apiWithDialog('admin/reset-password', {
userId: this.user.id,
}, undefined, ({ password }) => {
os.dialog({
type: 'success',
text: this.$t('newPasswordIs', { password })
});
});
},
async toggleSilence(v) {
const confirm = await os.dialog({
type: 'warning',
showCancelButton: true,
text: v ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'),
});
if (confirm.canceled) {
this.silenced = !v;
} else {
await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
await this.refreshUser();
}
},
async toggleSuspend(v) {
const confirm = await os.dialog({
type: 'warning',
showCancelButton: true,
text: v ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'),
});
if (confirm.canceled) {
this.suspended = !v;
} else {
await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
await this.refreshUser();
}
},
async toggleModerator(v) {
await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
await this.refreshUser();
},
async deleteAllFiles() {
const confirm = await os.dialog({
type: 'warning',
showCancelButton: true,
text: this.$t('deleteAllFilesConfirm'),
});
if (confirm.canceled) return;
const process = async () => {
await os.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
os.success();
};
await process().catch(e => {
os.dialog({
type: 'error',
text: e.toString()
});
});
await this.refreshUser();
},
acct
}
});
</script>
<style lang="scss" scoped>
.vrcsvlkm {
> ._section {
> .banner {
position: relative;
height: 100px;
background-color: #4c5e6d;
background-size: cover;
background-position: center;
border-radius: 8px;
> .avatar {
position: absolute;
top: 60px;
width: 64px;
height: 64px;
left: 0;
right: 0;
margin: 0 auto;
border: solid 4px var(--panel);
}
}
> .title {
text-align: center;
}
> .status {
text-align: center;
margin-top: 8px;
}
> .rawdata {
overflow: auto;
}
}
}
</style>

View File

@@ -1,206 +0,0 @@
<template>
<div class="vrcsvlkm" v-if="user && info">
<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
<section class="_card">
<div class="_title">
<mk-avatar class="avatar" :user="user"/>
<mk-user-name class="name" :user="user"/>
<span class="acct">@{{ user | acct }}</span>
<span class="staff" v-if="user.isAdmin"><fa :icon="faBookmark"/></span>
<span class="staff" v-if="user.isModerator"><fa :icon="farBookmark"/></span>
<span class="punished" v-if="user.isSilenced"><fa :icon="faMicrophoneSlash"/></span>
<span class="punished" v-if="user.isSuspended"><fa :icon="faSnowflake"/></span>
</div>
<div class="_content actions">
<div style="flex: 1; padding-left: 1em;">
<mk-switch v-if="user.host == null && $store.state.i.isAdmin && (this.moderator || !user.isAdmin)" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch>
<mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch>
<mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
</div>
<div style="flex: 1; padding-left: 1em;">
<mk-button @click="openProfile"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('profile')}}</mk-button>
<mk-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('updateRemoteUser') }}</mk-button>
<mk-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('resetPassword') }}</mk-button>
<mk-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</mk-button>
</div>
</div>
<div class="_content rawdata">
<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake, faTrashAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkSwitch from '../../components/ui/switch.vue';
import Progress from '../../scripts/loading';
export default Vue.extend({
components: {
MkButton,
MkSwitch,
},
data() {
return {
user: null,
info: null,
moderator: false,
silenced: false,
suspended: false,
faTimes, faBookmark, farBookmark, faKey, faSync, faMicrophoneSlash, faSnowflake, faTrashAlt, faExternalLinkSquareAlt
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
async fetch() {
Progress.start();
this.user = await this.$root.api('users/show', { userId: this.$route.params.user });
this.info = await this.$root.api('admin/show-user', { userId: this.$route.params.user });
this.moderator = this.info.isModerator;
this.silenced = this.info.isSilenced;
this.suspended = this.info.isSuspended;
Progress.done();
},
/** 処理対象ユーザーの情報を更新する */
async refreshUser() {
this.user = await this.$root.api('users/show', { userId: this.user.id });
this.info = await this.$root.api('admin/show-user', { userId: this.user.id });
},
openProfile() {
window.open(Vue.filter('userPage')(this.user, null, true), '_blank');
},
async updateRemoteUser() {
await this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
await this.refreshUser();
},
async resetPassword() {
const dialog = this.$root.dialog({
type: 'waiting',
iconOnly: true
});
this.$root.api('admin/reset-password', {
userId: this.user.id,
}).then(({ password }) => {
this.$root.dialog({
type: 'success',
text: this.$t('newPasswordIs', { password })
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
}).finally(() => {
dialog.close();
});
},
async toggleSilence() {
const confirm = await this.$root.dialog({
type: 'warning',
showCancelButton: true,
text: this.silenced ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'),
});
if (confirm.canceled) {
this.silenced = !this.silenced;
} else {
await this.$root.api(this.silenced ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id });
await this.refreshUser();
}
},
async toggleSuspend() {
const confirm = await this.$root.dialog({
type: 'warning',
showCancelButton: true,
text: this.suspended ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'),
});
if (confirm.canceled) {
this.suspended = !this.suspended;
} else {
await this.$root.api(this.suspended ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id });
await this.refreshUser();
}
},
async toggleModerator() {
await this.$root.api(this.moderator ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id });
await this.refreshUser();
},
async deleteAllFiles() {
const confirm = await this.$root.dialog({
type: 'warning',
showCancelButton: true,
text: this.$t('deleteAllFilesConfirm'),
});
if (confirm.canceled) return;
const process = async () => {
await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id });
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
await this.refreshUser();
},
}
});
</script>
<style lang="scss" scoped>
.vrcsvlkm {
display: flex;
flex-direction: column;
> ._card {
> .actions {
display: flex;
box-sizing: border-box;
text-align: left;
align-items: center;
margin-top: 16px;
margin-bottom: 16px;
}
> .rawdata {
> pre > code {
display: block;
width: 100%;
height: 100%;
}
}
}
}
</style>

View File

@@ -1,33 +1,33 @@
<template>
<div class="mk-instance-users">
<portal to="icon"><fa :icon="faUsers"/></portal>
<portal to="title">{{ $t('users') }}</portal>
<section class="_card _vMargin lookup">
<div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div>
<div class="_section">
<div class="_content">
<mk-input class="target" v-model="target" type="text" @enter="showUser()">
<span>{{ $t('usernameOrUserId') }}</span>
</mk-input>
<mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button>
<MkButton inline primary @click="addUser()"><Fa :icon="faPlus"/> {{ $t('addUser') }}</MkButton>
</div>
<div class="_footer">
<mk-button inline primary @click="searchUser()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button>
</div>
</section>
</div>
<section class="_card _vMargin users">
<div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div>
<div class="_section lookup">
<div class="_title"><Fa :icon="faSearch"/> {{ $t('lookup') }}</div>
<div class="_content">
<MkInput class="target" v-model:value="target" type="text" @enter="showUser()">
<span>{{ $t('usernameOrUserId') }}</span>
</MkInput>
<MkButton @click="showUser()" primary><Fa :icon="faSearch"/> {{ $t('lookup') }}</MkButton>
</div>
</div>
<div class="_section users">
<div class="_title"><Fa :icon="faUsers"/> {{ $t('users') }}</div>
<div class="_content">
<div class="inputs" style="display: flex;">
<mk-select v-model="sort" style="margin: 0; flex: 1;">
<MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
<template #label>{{ $t('sort') }}</template>
<option value="-createdAt">{{ $t('registeredDate') }} ({{ $t('ascendingOrder') }})</option>
<option value="+createdAt">{{ $t('registeredDate') }} ({{ $t('descendingOrder') }})</option>
<option value="-updatedAt">{{ $t('lastUsed') }} ({{ $t('ascendingOrder') }})</option>
<option value="+updatedAt">{{ $t('lastUsed') }} ({{ $t('descendingOrder') }})</option>
</mk-select>
<mk-select v-model="state" style="margin: 0; flex: 1;">
</MkSelect>
<MkSelect v-model:value="state" style="margin: 0; flex: 1;">
<template #label>{{ $t('state') }}</template>
<option value="all">{{ $t('all') }}</option>
<option value="available">{{ $t('normal') }}</option>
@@ -35,71 +35,62 @@
<option value="moderator">{{ $t('moderator') }}</option>
<option value="silenced">{{ $t('silence') }}</option>
<option value="suspended">{{ $t('suspend') }}</option>
</mk-select>
<mk-select v-model="origin" style="margin: 0; flex: 1;">
</MkSelect>
<MkSelect v-model:value="origin" style="margin: 0; flex: 1;">
<template #label>{{ $t('instance') }}</template>
<option value="combined">{{ $t('all') }}</option>
<option value="local">{{ $t('local') }}</option>
<option value="remote">{{ $t('remote') }}</option>
</mk-select>
</MkSelect>
</div>
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<mk-input v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @input="$refs.users.reload()">
<MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()">
<span>{{ $t('username') }}</span>
</mk-input>
<mk-input v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @input="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
</MkInput>
<MkInput v-model:value="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
<span>{{ $t('host') }}</span>
</mk-input>
</MkInput>
</div>
</div>
<div class="_content _list">
<mk-pagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false">
<button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" @click="show(user)">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
<MkPagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false">
<button class="user _panel _button _vMargin" v-for="user in items" :key="user.id" @click="show(user)">
<MkAvatar class="avatar" :user="user" :disable-link="true"/>
<div class="body">
<header>
<mk-user-name class="name" :user="user"/>
<span class="acct">@{{ user | acct }}</span>
<span class="staff" v-if="user.isAdmin"><fa :icon="faBookmark"/></span>
<span class="staff" v-if="user.isModerator"><fa :icon="farBookmark"/></span>
<span class="punished" v-if="user.isSilenced"><fa :icon="faMicrophoneSlash"/></span>
<span class="punished" v-if="user.isSuspended"><fa :icon="faSnowflake"/></span>
<MkUserName class="name" :user="user"/>
<span class="acct">@{{ acct(user) }}</span>
<span class="staff" v-if="user.isAdmin"><Fa :icon="faBookmark"/></span>
<span class="staff" v-if="user.isModerator"><Fa :icon="farBookmark"/></span>
<span class="punished" v-if="user.isSilenced"><Fa :icon="faMicrophoneSlash"/></span>
<span class="punished" v-if="user.isSuspended"><Fa :icon="faSnowflake"/></span>
</header>
<div>
<span>{{ $t('lastUsed') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
<span>{{ $t('lastUsed') }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $t('registeredDate') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
<span>{{ $t('registeredDate') }}: <MkTime :time="user.createdAt" mode="detail"/></span>
</div>
</div>
</button>
</mk-pagination>
</MkPagination>
</div>
<div class="_footer">
<mk-button inline primary @click="addUser()"><fa :icon="faPlus"/> {{ $t('addUser') }}</mk-button>
</div>
</section>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { faPlus, faUsers, faSearch, faBookmark, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons';
import parseAcct from '../../../misc/acct/parse';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkSelect from '../../components/ui/select.vue';
import MkPagination from '../../components/ui/pagination.vue';
import MkUserSelect from '../../components/user-select.vue';
export default Vue.extend({
metaInfo() {
return {
title: `${this.$t('users')} | ${this.$t('instance')}`
};
},
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkSelect from '@/components/ui/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import { acct } from '../../filters/user';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkInput,
@@ -109,6 +100,16 @@ export default Vue.extend({
data() {
return {
INFO: {
header: [{
title: this.$t('users'),
icon: faUsers
}],
action: {
icon: faSearch,
handler: this.searchUser
}
},
target: '',
sort: '+createdAt',
state: 'all',
@@ -147,12 +148,12 @@ export default Vue.extend({
/** テキストエリアのユーザーを解決する */
fetchUser() {
return new Promise((res) => {
const usernamePromise = this.$root.api('users/show', parseAcct(this.target));
const idPromise = this.$root.api('users/show', { userId: this.target });
const usernamePromise = os.api('users/show', parseAcct(this.target));
const idPromise = os.api('users/show', { userId: this.target });
let _notFound = false;
const notFound = () => {
if (_notFound) {
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('noSuchUser')
});
@@ -179,51 +180,39 @@ export default Vue.extend({
},
searchUser() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
os.selectUser().then(user => {
this.show(user);
});
},
async addUser() {
const { canceled: canceled1, result: username } = await this.$root.dialog({
const { canceled: canceled1, result: username } = await os.dialog({
title: this.$t('username'),
input: true
});
if (canceled1) return;
const { canceled: canceled2, result: password } = await this.$root.dialog({
const { canceled: canceled2, result: password } = await os.dialog({
title: this.$t('password'),
input: { type: 'password' }
});
if (canceled2) return;
const dialog = this.$root.dialog({
type: 'waiting',
iconOnly: true
});
this.$root.api('admin/accounts/create', {
os.apiWithDialog('admin/accounts/create', {
username: username,
password: password,
}).then(res => {
this.$refs.users.reload();
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e.id
});
}).finally(() => {
dialog.close();
});
},
async show(user) {
this.$router.push('./users/' + user.id);
}
os.popup(await import('./user-dialog.vue'), {
userId: user.id
}, {}, 'closed');
},
acct
}
});
</script>
@@ -232,28 +221,32 @@ export default Vue.extend({
.mk-instance-users {
> .users {
> ._content {
max-height: 300px;
overflow: auto;
> .users {
margin-top: var(--margin);
> .user {
display: flex;
width: 100%;
box-sizing: border-box;
text-align: left;
align-items: center;
padding: 16px;
&:hover {
color: var(--accent);
}
> .avatar {
width: 64px;
height: 64px;
width: 60px;
height: 60px;
}
> .body {
margin-left: 0.3em;
padding: 8px;
padding: 0 8px;
flex: 1;
@media (max-width 500px) {
@media (max-width: 500px) {
font-size: 14px;
}