summaryrefslogtreecommitdiff
path: root/src/client/pages/instance
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/pages/instance')
-rw-r--r--src/client/pages/instance/announcements.vue97
-rw-r--r--src/client/pages/instance/emoji-edit-dialog.vue116
-rw-r--r--src/client/pages/instance/emojis.vue307
-rw-r--r--src/client/pages/instance/federation.vue143
-rw-r--r--src/client/pages/instance/file-dialog.vue136
-rw-r--r--src/client/pages/instance/files.vue184
-rw-r--r--src/client/pages/instance/index.metrics.vue576
-rw-r--r--src/client/pages/instance/index.queue-chart.vue198
-rw-r--r--src/client/pages/instance/index.vue768
-rw-r--r--src/client/pages/instance/instance.vue164
-rw-r--r--src/client/pages/instance/logs.vue95
-rw-r--r--src/client/pages/instance/queue.chart.vue24
-rw-r--r--src/client/pages/instance/queue.vue51
-rw-r--r--src/client/pages/instance/relays.vue50
-rw-r--r--src/client/pages/instance/settings.vue284
-rw-r--r--src/client/pages/instance/user-dialog.vue233
-rw-r--r--src/client/pages/instance/users.user.vue206
-rw-r--r--src/client/pages/instance/users.vue165
18 files changed, 2002 insertions, 1795 deletions
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue
index 0e11e2932e..7abec88042 100644
--- a/src/client/pages/instance/announcements.vue
+++ b/src/client/pages/instance/announcements.vue
@@ -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>
diff --git a/src/client/pages/instance/emoji-edit-dialog.vue b/src/client/pages/instance/emoji-edit-dialog.vue
new file mode 100644
index 0000000000..ed81f15f6e
--- /dev/null
+++ b/src/client/pages/instance/emoji-edit-dialog.vue
@@ -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>
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue
index 25897ea7d9..465a9ebe00 100644
--- a/src/client/pages/instance/emojis.vue
+++ b/src/client/pages/instance/emojis.vue
@@ -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>
+ <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>
- </mk-pagination>
- </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>
+ </MkPagination>
</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">
+
+ <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="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 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>
- </div>
- <div class="_footer">
- <mk-button inline primary :disabled="selectedRemote == null" @click="im()"><fa :icon="faPlus"/> {{ $t('import') }}</mk-button>
+ </MkPagination>
</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;
}
}
}
diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue
index 8c5cbe2ff3..f2143fa003 100644
--- a/src/client/pages/instance/federation.vue
+++ b/src/client/pages/instance/federation.vue
@@ -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;
+ }
+
+ &.red {
+ color: #c54949;
+ }
- &.off {
- color: rgba(0, 0, 0, 0.5);
- }
- }
- }
+ &.off {
+ color: rgba(0, 0, 0, 0.5);
+ }
+ }
+ }
- > .status {
- display: flex;
- align-items: center;
- font-size: 90%;
+ > .status {
+ display: flex;
+ align-items: center;
+ font-size: 90%;
- > span {
- flex: 1;
-
- > .icon {
- margin-right: 6px;
- }
- }
- }
- }
+ > span {
+ flex: 1;
+
+ > .icon {
+ margin-right: 6px;
}
}
}
diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/instance/file-dialog.vue
new file mode 100644
index 0000000000..c03a691bfd
--- /dev/null
+++ b/src/client/pages/instance/file-dialog.vue
@@ -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>
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue
index 0bc1c81e6f..ea90e3b5cd 100644
--- a/src/client/pages/instance/files.vue
+++ b/src/client/pages/instance/files.vue
@@ -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>
diff --git a/src/client/pages/instance/index.metrics.vue b/src/client/pages/instance/index.metrics.vue
new file mode 100644
index 0000000000..f3060b29d5
--- /dev/null
+++ b/src/client/pages/instance/index.metrics.vue
@@ -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>
diff --git a/src/client/pages/instance/index.queue-chart.vue b/src/client/pages/instance/index.queue-chart.vue
deleted file mode 100644
index 3b7823d924..0000000000
--- a/src/client/pages/instance/index.queue-chart.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index f55a53b5f3..9383f256eb 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -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"/>
-
- <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>
+ <MkInstanceStats :chart-limit="300" :detailed="true" class="_vMargin" ref="stats"/>
- <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>
+ <MkContainer :body-togglable="true" class="_vMargin">
+ <template #header><Fa :icon="faInfoCircle"/>{{ $t('instanceInfo') }}</template>
- <div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
- <canvas ref="net"></canvas>
+ <div class="_content">
+ <div class="_keyValue"><b>Misskey</b><span>v{{ version }}</span></div>
</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 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>
- </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>
+
+ <MkContainer :body-togglable="true" :scrollable="true" class="_vMargin" style="height: 300px;">
+ <template #header><Fa :icon="faDatabase"/>{{ $t('database') }}</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="_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>
- </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>
+ </MkContainer>
</div>
- </mk-folder>
-
- <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>
+ </MkFolder>
+</div>
+<div v-if="page === 'logs'" class="_section">
+ <MkFolder>
+ <template #header><Fa :icon="faStream"/> {{ $t('logs') }}</template>
- <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
- });
- });
- },
-
- 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;
- },
+ bytes,
- 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>
diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue
index 30893f381b..97f85d3b1f 100644
--- a/src/client/pages/instance/instance.vue
+++ b/src/client/pages/instance/instance.vue
@@ -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;
diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue
new file mode 100644
index 0000000000..5549bd5a1a
--- /dev/null
+++ b/src/client/pages/instance/logs.vue
@@ -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>
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
index 8f66c8e486..742c2b7d3c 100644
--- a/src/client/pages/instance/queue.chart.vue
+++ b/src/client/pages/instance/queue.chart.vue
@@ -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>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue
index d9f12577e4..5dec95c670 100644
--- a/src/client/pages/instance/queue.vue
+++ b/src/client/pages/instance/queue.vue
@@ -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', {});
});
}
}
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue
index eaf6c0b682..82b7b006ed 100644
--- a/src/client/pages/instance/relays.vue
+++ b/src/client/pages/instance/relays.vue
@@ -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;
});
}
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index 0c0e506ab8..e8bf4a0bda 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -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
});
diff --git a/src/client/pages/instance/user-dialog.vue b/src/client/pages/instance/user-dialog.vue
new file mode 100644
index 0000000000..3cf30e115f
--- /dev/null
+++ b/src/client/pages/instance/user-dialog.vue
@@ -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>
diff --git a/src/client/pages/instance/users.user.vue b/src/client/pages/instance/users.user.vue
deleted file mode 100644
index 25f0260637..0000000000
--- a/src/client/pages/instance/users.user.vue
+++ /dev/null
@@ -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>
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue
index cf3786c965..b891ed8412 100644
--- a/src/client/pages/instance/users.vue
+++ b/src/client/pages/instance/users.vue
@@ -1,33 +1,33 @@
<template>
<div class="mk-instance-users">
- <portal to="icon"><fa :icon="faUsers"/></portal>
- <portal to="title">{{ $t('users') }}</portal>
+ <div class="_section">
+ <div class="_content">
+ <MkButton inline primary @click="addUser()"><Fa :icon="faPlus"/> {{ $t('addUser') }}</MkButton>
+ </div>
+ </div>
- <section class="_card _vMargin lookup">
- <div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div>
+ <div class="_section lookup">
+ <div class="_title"><Fa :icon="faSearch"/> {{ $t('lookup') }}</div>
<div class="_content">
- <mk-input class="target" v-model="target" type="text" @enter="showUser()">
+ <MkInput class="target" v-model:value="target" type="text" @enter="showUser()">
<span>{{ $t('usernameOrUserId') }}</span>
- </mk-input>
- <mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button>
- </div>
- <div class="_footer">
- <mk-button inline primary @click="searchUser()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button>
+ </MkInput>
+ <MkButton @click="showUser()" primary><Fa :icon="faSearch"/> {{ $t('lookup') }}</MkButton>
</div>
- </section>
+ </div>
- <section class="_card _vMargin users">
- <div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</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>
- </div>
- <div class="_footer">
- <mk-button inline primary @click="addUser()"><fa :icon="faPlus"/> {{ $t('addUser') }}</mk-button>
+ </MkPagination>
</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;
}