summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-04-22 22:29:33 +0900
committerGitHub <noreply@github.com>2021-04-22 22:29:33 +0900
commit246693b8484b72048cb515b76aa5f094f5fdeb56 (patch)
tree703f7636c363b480b20690495353691e09c98a27 /src
parentfix style (diff)
downloadsharkey-246693b8484b72048cb515b76aa5f094f5fdeb56.tar.gz
sharkey-246693b8484b72048cb515b76aa5f094f5fdeb56.tar.bz2
sharkey-246693b8484b72048cb515b76aa5f094f5fdeb56.zip
インスタンス管理画面作り直し (#7473)
* wip * wip * wip * wip
Diffstat (limited to 'src')
-rw-r--r--src/client/components/captcha.vue4
-rw-r--r--src/client/components/form/base.vue2
-rw-r--r--src/client/components/form/form.scss4
-rw-r--r--src/client/components/form/key-value-view.vue2
-rw-r--r--src/client/components/form/object-view.vue2
-rw-r--r--src/client/components/form/radios.vue3
-rw-r--r--src/client/components/form/suspense.vue9
-rw-r--r--src/client/components/instance-stats.vue252
-rw-r--r--src/client/components/signup.vue2
-rw-r--r--src/client/components/tab.vue1
-rw-r--r--src/client/components/ui/pagination.vue34
-rw-r--r--src/client/pages/instance/abuses.vue12
-rw-r--r--src/client/pages/instance/announcements.vue52
-rw-r--r--src/client/pages/instance/bot-protection.vue138
-rw-r--r--src/client/pages/instance/database.vue60
-rw-r--r--src/client/pages/instance/email-settings.vue127
-rw-r--r--src/client/pages/instance/emojis.vue213
-rw-r--r--src/client/pages/instance/federation.vue113
-rw-r--r--src/client/pages/instance/file-dialog.vue4
-rw-r--r--src/client/pages/instance/files-settings.vue92
-rw-r--r--src/client/pages/instance/files.vue8
-rw-r--r--src/client/pages/instance/index.vue356
-rw-r--r--src/client/pages/instance/instance-block.vue71
-rw-r--r--src/client/pages/instance/integrations-discord.vue85
-rw-r--r--src/client/pages/instance/integrations-github.vue85
-rw-r--r--src/client/pages/instance/integrations-twitter.vue85
-rw-r--r--src/client/pages/instance/integrations.vue73
-rw-r--r--src/client/pages/instance/metrics.vue (renamed from src/client/pages/instance/index.metrics.vue)241
-rw-r--r--src/client/pages/instance/object-storage.vue154
-rw-r--r--src/client/pages/instance/other-settings.vue68
-rw-r--r--src/client/pages/instance/overview.vue131
-rw-r--r--src/client/pages/instance/proxy-account.vue86
-rw-r--r--src/client/pages/instance/queue.chart.vue64
-rw-r--r--src/client/pages/instance/queue.vue24
-rw-r--r--src/client/pages/instance/relays.vue50
-rw-r--r--src/client/pages/instance/security.vue77
-rw-r--r--src/client/pages/instance/service-worker.vue84
-rw-r--r--src/client/pages/instance/settings.vue607
-rw-r--r--src/client/pages/instance/user-dialog.vue230
-rw-r--r--src/client/pages/instance/user.vue229
-rw-r--r--src/client/pages/instance/users.vue257
-rw-r--r--src/client/pages/user-info.vue70
-rw-r--r--src/client/pages/user/index.vue2
-rw-r--r--src/client/router.ts12
-rw-r--r--src/client/scripts/get-user-menu.ts8
-rw-r--r--src/client/scripts/lookup-user.ts37
-rw-r--r--src/client/ui/_common_/sidebar.vue63
-rw-r--r--src/client/ui/default.sidebar.vue63
48 files changed, 2570 insertions, 1876 deletions
diff --git a/src/client/components/captcha.vue b/src/client/components/captcha.vue
index 710fcd6169..26215df09d 100644
--- a/src/client/components/captcha.vue
+++ b/src/client/components/captcha.vue
@@ -18,7 +18,7 @@ type Captcha = {
getResponse(id: string): string;
};
-type CaptchaProvider = 'hcaptcha' | 'grecaptcha';
+type CaptchaProvider = 'hcaptcha' | 'recaptcha';
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
@@ -57,7 +57,7 @@ export default defineComponent({
src() {
const endpoint = ({
hcaptcha: 'https://hcaptcha.com/1',
- grecaptcha: 'https://www.recaptcha.net/recaptcha',
+ recaptcha: 'https://www.recaptcha.net/recaptcha',
} as Record<PropertyKey, unknown>)[this.provider];
return `${typeof endpoint == 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
diff --git a/src/client/components/form/base.vue b/src/client/components/form/base.vue
index 34deb39465..132942d527 100644
--- a/src/client/components/form/base.vue
+++ b/src/client/components/form/base.vue
@@ -24,6 +24,8 @@ export default defineComponent({
--formXPadding: 32px;
--formYPadding: 32px;
+ --formContentHMargin: 16px;
+
font-size: 95%;
line-height: 1.3em;
background: var(--bg);
diff --git a/src/client/components/form/form.scss b/src/client/components/form/form.scss
index 8c01fad727..05994ae650 100644
--- a/src/client/components/form/form.scss
+++ b/src/client/components/form/form.scss
@@ -30,7 +30,7 @@
top: var(--stickyTop, 0px);
z-index: 2;
margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1);
- padding: 8px calc(16px + var(--formXPadding)) 8px calc(16px + var(--formXPadding));
+ padding: 8px calc(var(--formContentHMargin) + var(--formXPadding)) 8px calc(var(--formContentHMargin) + var(--formXPadding));
background: var(--X17);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
@@ -42,7 +42,7 @@
}
._formCaption {
- padding: 8px 16px 0 16px;
+ padding: 8px var(--formContentHMargin) 0 var(--formContentHMargin);
}
._formItem {
diff --git a/src/client/components/form/key-value-view.vue b/src/client/components/form/key-value-view.vue
index 85f4febef9..ca4c09867f 100644
--- a/src/client/components/form/key-value-view.vue
+++ b/src/client/components/form/key-value-view.vue
@@ -20,7 +20,7 @@ export default defineComponent({
.anocepby {
display: flex;
align-items: center;
- padding: 14px 16px;
+ padding: 14px var(--formContentHMargin);
> .key {
margin-right: 12px;
diff --git a/src/client/components/form/object-view.vue b/src/client/components/form/object-view.vue
index cbd4186e56..59fb62b5e6 100644
--- a/src/client/components/form/object-view.vue
+++ b/src/client/components/form/object-view.vue
@@ -75,7 +75,7 @@ export default defineComponent({
max-width: 100%;
min-height: 130px;
margin: 0;
- padding: 16px;
+ padding: 16px var(--formContentHMargin);
box-sizing: border-box;
font: inherit;
font-weight: normal;
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
index 3daa7e5bbd..4cfb7c247e 100644
--- a/src/client/components/form/radios.vue
+++ b/src/client/components/form/radios.vue
@@ -18,6 +18,9 @@ export default defineComponent({
}
},
watch: {
+ modelValue() {
+ this.value = this.modelValue;
+ },
value() {
this.$emit('update:modelValue', this.value);
}
diff --git a/src/client/components/form/suspense.vue b/src/client/components/form/suspense.vue
index 6a8282733f..2a48faccb3 100644
--- a/src/client/components/form/suspense.vue
+++ b/src/client/components/form/suspense.vue
@@ -5,9 +5,9 @@
<MkLoading/>
</div>
</div>
- <FormGroup v-else-if="resolved" class="_formItem">
+ <div v-else-if="resolved" class="_formItem">
<slot :result="result"></slot>
- </FormGroup>
+ </div>
<div class="_formItem" v-else>
<div class="_formPanel">
error!
@@ -20,13 +20,8 @@
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue';
import './form.scss';
-import FormGroup from './group.vue';
export default defineComponent({
- components: {
- FormGroup,
- },
-
props: {
p: {
type: Function as PropType<() => Promise<any>>,
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index aa01f1c806..432c9a1bb9 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -1,123 +1,35 @@
<template>
-<div class="zbcjwnqg" v-size="{ max: [550, 1000] }">
- <div class="stats" v-if="info">
- <div class="_panel">
- <div>
- <b><i class="fas fa-user"></i>{{ $ts.users }}</b>
- <small>{{ $ts.local }}</small>
- </div>
- <div>
- <dl class="total">
- <dt>{{ $ts.total }}</dt>
- <dd>{{ number(info.originalUsersCount) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: usersLocalDoD > 0 }">
- <dt>{{ $ts.dayOverDayChanges }}</dt>
- <dd>{{ number(usersLocalDoD) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: usersLocalWoW > 0 }">
- <dt>{{ $ts.weekOverWeekChanges }}</dt>
- <dd>{{ number(usersLocalWoW) }}</dd>
- </dl>
- </div>
- </div>
- <div class="_panel">
- <div>
- <b><i class="fas fa-user"></i>{{ $ts.users }}</b>
- <small>{{ $ts.remote }}</small>
- </div>
- <div>
- <dl class="total">
- <dt>{{ $ts.total }}</dt>
- <dd>{{ number((info.usersCount - info.originalUsersCount)) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: usersRemoteDoD > 0 }">
- <dt>{{ $ts.dayOverDayChanges }}</dt>
- <dd>{{ number(usersRemoteDoD) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: usersRemoteWoW > 0 }">
- <dt>{{ $ts.weekOverWeekChanges }}</dt>
- <dd>{{ number(usersRemoteWoW) }}</dd>
- </dl>
- </div>
- </div>
- <div class="_panel">
- <div>
- <b><i class="fas fa-pencil-alt"></i>{{ $ts.notes }}</b>
- <small>{{ $ts.local }}</small>
- </div>
- <div>
- <dl class="total">
- <dt>{{ $ts.total }}</dt>
- <dd>{{ number(info.originalNotesCount) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: notesLocalDoD > 0 }">
- <dt>{{ $ts.dayOverDayChanges }}</dt>
- <dd>{{ number(notesLocalDoD) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: notesLocalWoW > 0 }">
- <dt>{{ $ts.weekOverWeekChanges }}</dt>
- <dd>{{ number(notesLocalWoW) }}</dd>
- </dl>
- </div>
- </div>
- <div class="_panel">
- <div>
- <b><i class="fas fa-pencil-alt"></i>{{ $ts.notes }}</b>
- <small>{{ $ts.remote }}</small>
- </div>
- <div>
- <dl class="total">
- <dt>{{ $ts.total }}</dt>
- <dd>{{ number((info.notesCount - info.originalNotesCount)) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: notesRemoteDoD > 0 }">
- <dt>{{ $ts.dayOverDayChanges }}</dt>
- <dd>{{ number(notesRemoteDoD) }}</dd>
- </dl>
- <dl class="diff" :class="{ inc: notesRemoteWoW > 0 }">
- <dt>{{ $ts.weekOverWeekChanges }}</dt>
- <dd>{{ number(notesRemoteWoW) }}</dd>
- </dl>
- </div>
- </div>
+<div class="zbcjwnqg" style="margin-top: -8px;">
+ <div class="selects" style="display: flex;">
+ <MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
+ <optgroup :label="$ts.federation">
+ <option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option>
+ <option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option>
+ </optgroup>
+ <optgroup :label="$ts.users">
+ <option value="users">{{ $ts._charts.usersIncDec }}</option>
+ <option value="users-total">{{ $ts._charts.usersTotal }}</option>
+ <option value="active-users">{{ $ts._charts.activeUsers }}</option>
+ </optgroup>
+ <optgroup :label="$ts.notes">
+ <option value="notes">{{ $ts._charts.notesIncDec }}</option>
+ <option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
+ <option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
+ <option value="notes-total">{{ $ts._charts.notesTotal }}</option>
+ </optgroup>
+ <optgroup :label="$ts.drive">
+ <option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
+ <option value="drive-files-total">{{ $ts._charts.filesTotal }}</option>
+ <option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
+ <option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
+ </optgroup>
+ </MkSelect>
+ <MkSelect v-model:value="chartSpan" style="margin: 0;">
+ <option value="hour">{{ $ts.perHour }}</option>
+ <option value="day">{{ $ts.perDay }}</option>
+ </MkSelect>
</div>
-
- <section class="_card">
- <div class="_title" style="position: relative;"><i class="fas fa-chart-bar"></i> {{ $ts.statistics }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><i class="fas fa-sync"></i></button></div>
- <div class="_content" style="margin-top: -8px;">
- <div class="selects" style="display: flex;">
- <MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;">
- <optgroup :label="$ts.federation">
- <option value="federation-instances">{{ $ts._charts.federationInstancesIncDec }}</option>
- <option value="federation-instances-total">{{ $ts._charts.federationInstancesTotal }}</option>
- </optgroup>
- <optgroup :label="$ts.users">
- <option value="users">{{ $ts._charts.usersIncDec }}</option>
- <option value="users-total">{{ $ts._charts.usersTotal }}</option>
- <option value="active-users">{{ $ts._charts.activeUsers }}</option>
- </optgroup>
- <optgroup :label="$ts.notes">
- <option value="notes">{{ $ts._charts.notesIncDec }}</option>
- <option value="local-notes">{{ $ts._charts.localNotesIncDec }}</option>
- <option value="remote-notes">{{ $ts._charts.remoteNotesIncDec }}</option>
- <option value="notes-total">{{ $ts._charts.notesTotal }}</option>
- </optgroup>
- <optgroup :label="$ts.drive">
- <option value="drive-files">{{ $ts._charts.filesIncDec }}</option>
- <option value="drive-files-total">{{ $ts._charts.filesTotal }}</option>
- <option value="drive">{{ $ts._charts.storageUsageIncDec }}</option>
- <option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
- </optgroup>
- </MkSelect>
- <MkSelect v-model:value="chartSpan" style="margin: 0;">
- <option value="hour">{{ $ts.perHour }}</option>
- <option value="day">{{ $ts.perDay }}</option>
- </MkSelect>
- </div>
- <canvas ref="chart"></canvas>
- </div>
- </section>
+ <canvas ref="chart"></canvas>
</div>
</template>
@@ -158,7 +70,6 @@ export default defineComponent({
data() {
return {
- info: null,
notesLocalWoW: 0,
notesLocalDoD: 0,
notesRemoteWoW: 0,
@@ -216,8 +127,6 @@ export default defineComponent({
},
async created() {
- this.info = await os.api('stats');
-
this.now = new Date();
this.fetchChart();
@@ -256,15 +165,6 @@ export default defineComponent({
}
};
- this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7];
- this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1];
- this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7];
- this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1];
- this.usersLocalWoW = this.info.originalUsersCount - chart.perDay.users.local.total[7];
- this.usersLocalDoD = this.info.originalUsersCount - chart.perDay.users.local.total[1];
- this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7];
- this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1];
-
this.chart = chart;
this.renderChart();
@@ -300,10 +200,10 @@ export default defineComponent({
aspectRatio: 2.5,
layout: {
padding: {
- left: 0,
- right: 0,
+ left: 16,
+ right: 16,
top: 16,
- bottom: 0
+ bottom: 8
}
},
legend: {
@@ -630,90 +530,8 @@ export default defineComponent({
<style lang="scss" scoped>
.zbcjwnqg {
- &.max-width_1000px {
- > .stats {
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 1fr 1fr;
- }
- }
-
- &.max-width_550px {
- > .stats {
- grid-template-columns: 1fr;
- grid-template-rows: 1fr 1fr 1fr 1fr;
- }
- }
-
- > .stats {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr 1fr;
- grid-template-rows: 1fr;
- gap: var(--margin);
- margin-bottom: var(--margin);
- font-size: 90%;
-
- > div {
- display: flex;
- box-sizing: border-box;
- padding: 16px 20px;
-
- > div {
- width: 50%;
-
- &:first-child {
- > b {
- display: block;
-
- > i {
- width: 16px;
- margin-right: 8px;
- }
- }
-
- > small {
- margin-left: 16px + 8px;
- opacity: 0.7;
- }
- }
-
- &:last-child {
- > dl {
- display: flex;
- margin: 0;
- line-height: 1.5em;
-
- > dt,
- > dd {
- width: 50%;
- margin: 0;
- }
-
- > dd {
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- }
-
- &.total {
- > dt,
- > dd {
- font-weight: bold;
- }
- }
-
- &.diff.inc {
- > dd {
- color: #82c11c;
-
- &:before {
- content: "+";
- }
- }
- }
- }
- }
- }
- }
+ > .selects {
+ padding: 8px 16px 0 16px;
}
}
</style>
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
index 7b40561adf..671642b291 100644
--- a/src/client/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -45,7 +45,7 @@
</I18n>
</label>
<captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model:value="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
- <captcha v-if="meta.enableRecaptcha" class="captcha" provider="grecaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
+ <captcha v-if="meta.enableRecaptcha" class="captcha" provider="recaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
<MkButton type="submit" :disabled="shouldDisableSubmitting" primary>{{ $ts.start }}</MkButton>
</template>
</form>
diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
index aca4d32a22..96cbe50fb1 100644
--- a/src/client/components/tab.vue
+++ b/src/client/components/tab.vue
@@ -29,6 +29,7 @@ export default defineComponent({
<style lang="scss">
.pxhvhrfw {
display: flex;
+ font-size: 90%;
> button {
flex: 1;
diff --git a/src/client/components/ui/pagination.vue b/src/client/components/ui/pagination.vue
index 13181d39e2..ac8ed01e12 100644
--- a/src/client/components/ui/pagination.vue
+++ b/src/client/components/ui/pagination.vue
@@ -1,16 +1,23 @@
<template>
-<div class="cxiknjgy">
- <slot :items="items"></slot>
- <div class="empty" v-if="empty" key="_empty_">
+<transition name="fade" mode="out-in">
+ <MkLoading v-if="fetching"/>
+
+ <MkError v-else-if="error" @retry="init()"/>
+
+ <div class="empty" v-else-if="empty" key="_empty_">
<slot name="empty"></slot>
</div>
- <div class="more" v-show="more" key="_more_">
- <MkButton class="button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </MkButton>
+
+ <div v-else class="cxiknjgy">
+ <slot :items="items"></slot>
+ <div class="more" v-show="more" key="_more_">
+ <MkButton class="button" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
+ <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
+ <template v-if="moreFetching"><MkLoading inline/></template>
+ </MkButton>
+ </div>
</div>
-</div>
+</transition>
</template>
<script lang="ts">
@@ -36,6 +43,15 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.125s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
.cxiknjgy {
> .more > .button {
margin-left: auto;
diff --git a/src/client/pages/instance/abuses.vue b/src/client/pages/instance/abuses.vue
index 7666bc1a44..73196027dc 100644
--- a/src/client/pages/instance/abuses.vue
+++ b/src/client/pages/instance/abuses.vue
@@ -1,5 +1,5 @@
<template>
-<div class="">
+<div class="lcixvhis">
<div class="_section reports">
<div class="_content">
<div class="inputs" style="display: flex;">
@@ -80,6 +80,8 @@ export default defineComponent({
MkPagination,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
@@ -117,6 +119,10 @@ export default defineComponent({
},
},
+ mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
methods: {
acct,
@@ -132,6 +138,10 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
+.lcixvhis {
+ margin: var(--margin);
+}
+
.bcekxzvu {
> .target {
display: flex;
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue
index 6a00476f9f..ac0e9d5135 100644
--- a/src/client/pages/instance/announcements.vue
+++ b/src/client/pages/instance/announcements.vue
@@ -1,28 +1,24 @@
<template>
<div class="ztgjmzrw">
- <div class="_section">
- <div class="_content">
- <MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
- <section class="_card _gap announcements" v-for="announcement in announcements">
- <div class="_content announcement">
- <MkInput v-model:value="announcement.title">
- <span>{{ $ts.title }}</span>
- </MkInput>
- <MkTextarea v-model:value="announcement.text">
- <span>{{ $ts.text }}</span>
- </MkTextarea>
- <MkInput v-model:value="announcement.imageUrl">
- <span>{{ $ts.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><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
- </div>
- </div>
- </section>
+ <MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
+ <section class="_card _gap announcements" v-for="announcement in announcements">
+ <div class="_content announcement">
+ <MkInput v-model:value="announcement.title">
+ <span>{{ $ts.title }}</span>
+ </MkInput>
+ <MkTextarea v-model:value="announcement.text">
+ <span>{{ $ts.text }}</span>
+ </MkTextarea>
+ <MkInput v-model:value="announcement.imageUrl">
+ <span>{{ $ts.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><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+ <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+ </div>
</div>
- </div>
+ </section>
</div>
</template>
@@ -41,6 +37,8 @@ export default defineComponent({
MkTextarea,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
@@ -57,6 +55,10 @@ export default defineComponent({
});
},
+ mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
methods: {
add() {
this.announcements.unshift({
@@ -109,3 +111,9 @@ export default defineComponent({
}
});
</script>
+
+<style lang="scss" scoped>
+.ztgjmzrw {
+ margin: var(--margin);
+}
+</style>
diff --git a/src/client/pages/instance/bot-protection.vue b/src/client/pages/instance/bot-protection.vue
new file mode 100644
index 0000000000..449b8a233d
--- /dev/null
+++ b/src/client/pages/instance/bot-protection.vue
@@ -0,0 +1,138 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormRadios v-model="provider">
+ <template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template>
+ <option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
+ <option value="hcaptcha">hCaptcha</option>
+ <option value="recaptcha">reCAPTCHA</option>
+ </FormRadios>
+
+ <template v-if="provider === 'hcaptcha'">
+ <div class="_formItem _formNoConcat" v-sticky-container>
+ <div class="_formLabel">hCaptcha</div>
+ <div class="main">
+ <FormInput v-model:value="hcaptchaSiteKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <span>{{ $ts.hcaptchaSiteKey }}</span>
+ </FormInput>
+ <FormInput v-model:value="hcaptchaSecretKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <span>{{ $ts.hcaptchaSecretKey }}</span>
+ </FormInput>
+ </div>
+ </div>
+ <div class="_formItem _formNoConcat" v-sticky-container>
+ <div class="_formLabel">{{ $ts.preview }}</div>
+ <div class="_formPanel" style="padding: var(--formContentHMargin);">
+ <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
+ </div>
+ </div>
+ </template>
+ <template v-else-if="provider === 'recaptcha'">
+ <div class="_formItem _formNoConcat" v-sticky-container>
+ <div class="_formLabel">reCAPTCHA</div>
+ <div class="main">
+ <FormInput v-model:value="recaptchaSiteKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <span>{{ $ts.recaptchaSiteKey }}</span>
+ </FormInput>
+ <FormInput v-model:value="recaptchaSecretKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <span>{{ $ts.recaptchaSecretKey }}</span>
+ </FormInput>
+ </div>
+ </div>
+ <div v-if="recaptchaSiteKey" class="_formItem _formNoConcat" v-sticky-container>
+ <div class="_formLabel">{{ $ts.preview }}</div>
+ <div class="_formPanel" style="padding: var(--formContentHMargin);">
+ <MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
+ </div>
+ </div>
+ </template>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormRadios from '@client/components/form/radios.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormRadios,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormInfo,
+ FormSuspense,
+ MkCaptcha: defineAsyncComponent(() => import('@client/components/captcha.vue')),
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.botProtection,
+ icon: 'fas fa-shield-alt'
+ },
+ provider: null,
+ enableHcaptcha: false,
+ hcaptchaSiteKey: null,
+ hcaptchaSecretKey: null,
+ enableRecaptcha: false,
+ recaptchaSiteKey: null,
+ recaptchaSecretKey: null,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableHcaptcha = meta.enableHcaptcha;
+ this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
+ this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
+ this.enableRecaptcha = meta.enableRecaptcha;
+ this.recaptchaSiteKey = meta.recaptchaSiteKey;
+ this.recaptchaSecretKey = meta.recaptchaSecretKey;
+
+ this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
+
+ this.$watch(() => this.provider, () => {
+ this.enableHcaptcha = this.provider === 'hcaptcha';
+ this.enableRecaptcha = this.provider === 'recaptcha';
+ });
+ },
+
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ enableHcaptcha: this.enableHcaptcha,
+ hcaptchaSiteKey: this.hcaptchaSiteKey,
+ hcaptchaSecretKey: this.hcaptchaSecretKey,
+ enableRecaptcha: this.enableRecaptcha,
+ recaptchaSiteKey: this.recaptchaSiteKey,
+ recaptchaSecretKey: this.recaptchaSecretKey,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/database.vue b/src/client/pages/instance/database.vue
new file mode 100644
index 0000000000..a41d61ce2b
--- /dev/null
+++ b/src/client/pages/instance/database.vue
@@ -0,0 +1,60 @@
+<template>
+<FormBase>
+ <FormSuspense :p="databasePromiseFactory" v-slot="{ result: database }">
+ <FormGroup v-for="table in database" :key="table[0]">
+ <template #label>{{ table[0] }}</template>
+ <FormKeyValueView>
+ <template #key>Size</template>
+ <template #value>{{ bytes(table[1].size) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>Records</template>
+ <template #value>{{ number(table[1].count) }}</template>
+ </FormKeyValueView>
+ </FormGroup>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormLink from '@client/components/form/link.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import bytes from '@client/filters/bytes';
+import number from '@client/filters/number';
+
+export default defineComponent({
+ components: {
+ FormSuspense,
+ FormKeyValueView,
+ FormBase,
+ FormGroup,
+ FormLink,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.database,
+ icon: 'fas fa-database'
+ },
+ databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
+ }
+ },
+
+ mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ bytes, number,
+ }
+});
+</script>
diff --git a/src/client/pages/instance/email-settings.vue b/src/client/pages/instance/email-settings.vue
new file mode 100644
index 0000000000..9965a1420f
--- /dev/null
+++ b/src/client/pages/instance/email-settings.vue
@@ -0,0 +1,127 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSwitch v-model:value="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
+
+ <template v-if="enableEmail">
+ <FormInput v-model:value="email" type="email">
+ <span>{{ $ts.emailAddress }}</span>
+ </FormInput>
+
+ <div class="_formItem _formNoConcat" v-sticky-container>
+ <div class="_formLabel">{{ $ts.smtpConfig }}</div>
+ <div class="main">
+ <FormInput v-model:value="smtpHost">
+ <span>{{ $ts.smtpHost }}</span>
+ </FormInput>
+ <FormInput v-model:value="smtpPort" type="number">
+ <span>{{ $ts.smtpPort }}</span>
+ </FormInput>
+ <FormInput v-model:value="smtpUser">
+ <span>{{ $ts.smtpUser }}</span>
+ </FormInput>
+ <FormInput v-model:value="smtpPass" type="password">
+ <span>{{ $ts.smtpPass }}</span>
+ </FormInput>
+ <FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
+ <FormSwitch v-model:value="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
+ </div>
+ </div>
+
+ <FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton>
+ </template>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormInfo,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.emailServer,
+ icon: 'fas fa-envelope'
+ },
+ enableEmail: false,
+ email: null,
+ smtpSecure: false,
+ smtpHost: '',
+ smtpPort: 0,
+ smtpUser: '',
+ smtpPass: '',
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableEmail = meta.enableEmail;
+ this.email = meta.email;
+ this.smtpSecure = meta.smtpSecure;
+ this.smtpHost = meta.smtpHost;
+ this.smtpPort = meta.smtpPort;
+ this.smtpUser = meta.smtpUser;
+ this.smtpPass = meta.smtpPass;
+ },
+
+ async testEmail() {
+ const { canceled, result: destination } = await os.dialog({
+ title: this.$ts.destination,
+ input: {
+ placeholder: this.$instance.maintainerEmail
+ }
+ });
+ if (canceled) return;
+ os.apiWithDialog('admin/send-email', {
+ to: destination,
+ subject: 'Test email',
+ text: 'Yo'
+ });
+ },
+
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ enableEmail: this.enableEmail,
+ email: this.email,
+ smtpSecure: this.smtpSecure,
+ smtpHost: this.smtpHost,
+ smtpPort: this.smtpPort,
+ smtpUser: this.smtpUser,
+ smtpPass: this.smtpPass,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue
index 88bebb40e0..fd641703cb 100644
--- a/src/client/pages/instance/emojis.vue
+++ b/src/client/pages/instance/emojis.vue
@@ -1,50 +1,46 @@
<template>
-<div class="mk-instance-emojis">
- <div class="_section" style="padding: 0;">
- <MkTab v-model:value="tab">
- <option value="local">{{ $ts.local }}</option>
- <option value="remote">{{ $ts.remote }}</option>
- </MkTab>
- </div>
+<div class="ogwlenmc">
+ <MkTab v-model:value="tab">
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ </MkTab>
- <div class="_section">
- <div class="local" v-if="tab === 'local'">
- <MkButton primary @click="add" style="margin: 0 auto var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.addEmoji }}</MkButton>
- <MkInput v-model:value="query" :debounce="true" type="search"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
- <MkPagination :pagination="pagination" ref="emojis">
- <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
- <template #default="{items}">
- <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">
- <div class="name">{{ emoji.name }}</div>
- <div class="info">{{ emoji.category }}</div>
- </div>
- </button>
- </div>
- </template>
- </MkPagination>
- </div>
+ <div class="local" v-if="tab === 'local'">
+ <MkButton primary @click="add" style="margin: var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.addEmoji }}</MkButton>
+ <MkInput v-model:value="query" :debounce="true" type="search" style="margin: var(--margin);"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
+ <MkPagination :pagination="pagination" ref="emojis">
+ <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+ <template #default="{items}">
+ <div class="ldhfsamy">
+ <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">
+ <div class="name _monospace">{{ emoji.name }}</div>
+ <div class="info">{{ emoji.category }}</div>
+ </div>
+ </button>
+ </div>
+ </template>
+ </MkPagination>
+ </div>
- <div class="remote" v-else-if="tab === 'remote'">
- <MkInput v-model:value="queryRemote" :debounce="true" type="search"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
- <MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput>
- <MkPagination :pagination="remotePagination" ref="remoteEmojis">
- <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
- <template #default="{items}">
- <div class="emojis">
- <div class="emoji _panel _button" v-for="emoji in items" :key="emoji.id" @click="remoteMenu(emoji, $event)">
- <img :src="emoji.url" class="img" :alt="emoji.name"/>
- <div class="body">
- <div class="name">{{ emoji.name }}</div>
- <div class="info">{{ emoji.host }}</div>
- </div>
+ <div class="remote" v-else-if="tab === 'remote'">
+ <MkInput v-model:value="queryRemote" :debounce="true" type="search" style="margin: var(--margin);"><template #icon><i class="fas fa-search"></i></template><span>{{ $ts.search }}</span></MkInput>
+ <MkInput v-model:value="host" :debounce="true" style="margin: var(--margin);"><span>{{ $ts.host }}</span></MkInput>
+ <MkPagination :pagination="remotePagination" ref="remoteEmojis">
+ <template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
+ <template #default="{items}">
+ <div class="ldhfsamy">
+ <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">
+ <div class="name _monospace">{{ emoji.name }}</div>
+ <div class="info">{{ emoji.host }}</div>
</div>
</div>
- </template>
- </MkPagination>
- </div>
+ </div>
+ </template>
+ </MkPagination>
</div>
</div>
</template>
@@ -67,6 +63,8 @@ export default defineComponent({
MkPagination,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
@@ -99,6 +97,10 @@ export default defineComponent({
}
},
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
methods: {
async add(e) {
const files = await selectFile(e.currentTarget || e.target, null, true);
@@ -150,85 +152,86 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-.mk-instance-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;
+.ogwlenmc {
+ > .local {
+ .ldhfsamy {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+ grid-gap: 12px;
+ margin: var(--margin);
+
+ > .emoji {
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ text-align: left;
- &:hover {
- color: var(--accent);
- }
+ &:hover {
+ color: var(--accent);
+ }
- > .img {
- width: 42px;
- height: 42px;
- }
+ > .img {
+ width: 42px;
+ height: 42px;
+ }
- > .body {
- padding: 0 0 0 8px;
- white-space: nowrap;
- overflow: hidden;
+ > .body {
+ padding: 0 0 0 8px;
+ white-space: nowrap;
+ overflow: hidden;
- > .name {
- text-overflow: ellipsis;
- overflow: hidden;
- }
+ > .name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
- > .info {
- opacity: 0.5;
- text-overflow: ellipsis;
- overflow: hidden;
- }
+ > .info {
+ opacity: 0.5;
+ text-overflow: ellipsis;
+ overflow: hidden;
}
}
}
}
+ }
- > .remote {
- .emojis {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
- grid-gap: var(--margin);
+ > .remote {
+ .ldhfsamy {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
+ grid-gap: 12px;
+ margin: var(--margin);
- > .emoji {
- display: flex;
- align-items: center;
- padding: 12px;
- text-align: left;
+ > .emoji {
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ text-align: left;
- &:hover {
- color: var(--accent);
- }
+ &:hover {
+ color: var(--accent);
+ }
- > .img {
- width: 32px;
- height: 32px;
- }
+ > .img {
+ width: 32px;
+ height: 32px;
+ }
- > .body {
- padding: 0 0 0 8px;
- white-space: nowrap;
- overflow: hidden;
+ > .body {
+ padding: 0 0 0 8px;
+ white-space: nowrap;
+ overflow: hidden;
- > .name {
- text-overflow: ellipsis;
- overflow: hidden;
- }
+ > .name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
- > .info {
- opacity: 0.5;
- text-overflow: ellipsis;
- overflow: hidden;
- }
+ > .info {
+ opacity: 0.5;
+ font-size: 90%;
+ text-overflow: ellipsis;
+ overflow: hidden;
}
}
}
diff --git a/src/client/pages/instance/federation.vue b/src/client/pages/instance/federation.vue
index 1b69fc291e..96f72fed44 100644
--- a/src/client/pages/instance/federation.vue
+++ b/src/client/pages/instance/federation.vue
@@ -1,60 +1,55 @@
<template>
-<div>
- <div class="_section">
- <div class="_content">
- <MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput>
- <div class="inputs" style="display: flex;">
- <MkSelect v-model:value="state" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.state }}</template>
- <option value="all">{{ $ts.all }}</option>
- <option value="federating">{{ $ts.federating }}</option>
- <option value="subscribing">{{ $ts.subscribing }}</option>
- <option value="publishing">{{ $ts.publishing }}</option>
- <option value="suspended">{{ $ts.suspended }}</option>
- <option value="blocked">{{ $ts.blocked }}</option>
- <option value="notResponding">{{ $ts.notResponding }}</option>
- </MkSelect>
- <MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.sort }}</template>
- <option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
- <option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
- <option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
- <option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
- <option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
- <option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
- <option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
- <option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
- <option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
- <option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
- <option value="+caughtAt">{{ $ts.caughtAt }} ({{ $ts.descendingOrder }})</option>
- <option value="-caughtAt">{{ $ts.caughtAt }} ({{ $ts.ascendingOrder }})</option>
- <option value="+lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.descendingOrder }})</option>
- <option value="-lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.ascendingOrder }})</option>
- <option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option>
- <option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option>
- <option value="+driveFiles">{{ $ts.driveFiles }} ({{ $ts.descendingOrder }})</option>
- <option value="-driveFiles">{{ $ts.driveFiles }} ({{ $ts.ascendingOrder }})</option>
- </MkSelect>
- </div>
+<div class="enuoauvw">
+ <div class="query">
+ <MkInput v-model:value="host" :debounce="true"><span>{{ $ts.host }}</span></MkInput>
+ <div class="inputs" style="display: flex;">
+ <MkSelect v-model:value="state" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.state }}</template>
+ <option value="all">{{ $ts.all }}</option>
+ <option value="federating">{{ $ts.federating }}</option>
+ <option value="subscribing">{{ $ts.subscribing }}</option>
+ <option value="publishing">{{ $ts.publishing }}</option>
+ <option value="suspended">{{ $ts.suspended }}</option>
+ <option value="blocked">{{ $ts.blocked }}</option>
+ <option value="notResponding">{{ $ts.notResponding }}</option>
+ </MkSelect>
+ <MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.sort }}</template>
+ <option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
+ <option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
+ <option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
+ <option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
+ <option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
+ <option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+caughtAt">{{ $ts.caughtAt }} ({{ $ts.descendingOrder }})</option>
+ <option value="-caughtAt">{{ $ts.caughtAt }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.descendingOrder }})</option>
+ <option value="-lastCommunicatedAt">{{ $ts.lastCommunicatedAt }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option>
+ <option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+driveFiles">{{ $ts.driveFiles }} ({{ $ts.descendingOrder }})</option>
+ <option value="-driveFiles">{{ $ts.driveFiles }} ({{ $ts.ascendingOrder }})</option>
+ </MkSelect>
</div>
</div>
- <div class="_section">
- <div class="_content">
- <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"><i class="fas fa-circle indicator" :class="getStatus(instance)"></i><b>{{ instance.host }}</b></div>
- <div class="status">
- <span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
- <span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span>
- <span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span>
- <span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span>
- <span class="lastCommunicatedAt"><i class="fas fa-exchange-alt icon"></i><MkTime :time="instance.lastCommunicatedAt"/></span>
- <span class="latestStatus"><i class="fas fa-traffic-light icon"></i>{{ instance.latestStatus || '-' }}</span>
- </div>
- </div>
- </MkPagination>
+
+ <MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
+ <div class="ppgwaixt _block" v-for="instance in items" :key="instance.id" @click="info(instance)">
+ <div class="host"><i class="fas fa-circle indicator" :class="getStatus(instance)"></i><b>{{ instance.host }}</b></div>
+ <div class="status">
+ <span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
+ <span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span>
+ <span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span>
+ <span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span>
+ <span class="lastCommunicatedAt"><i class="fas fa-exchange-alt icon"></i><MkTime :time="instance.lastCommunicatedAt"/></span>
+ <span class="latestStatus"><i class="fas fa-traffic-light icon"></i>{{ instance.latestStatus || '-' }}</span>
+ </div>
</div>
- </div>
+ </MkPagination>
</div>
</template>
@@ -76,6 +71,8 @@ export default defineComponent({
MkPagination,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
@@ -114,6 +111,10 @@ export default defineComponent({
}
},
+ mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
methods: {
getStatus(instance) {
if (instance.isSuspended) return 'off';
@@ -131,6 +132,12 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
+.enuoauvw {
+ > .query {
+ margin: var(--margin);
+ }
+}
+
.ppgwaixt {
cursor: pointer;
padding: 16px;
diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/instance/file-dialog.vue
index 1220a5193f..ae6755465c 100644
--- a/src/client/pages/instance/file-dialog.vue
+++ b/src/client/pages/instance/file-dialog.vue
@@ -82,9 +82,7 @@ export default defineComponent({
},
showUser() {
- os.popup(import('./user-dialog.vue'), {
- userId: this.file.userId
- }, {}, 'closed');
+ os.pageWindow(`/instance/user/${this.file.userId}`);
},
async del() {
diff --git a/src/client/pages/instance/files-settings.vue b/src/client/pages/instance/files-settings.vue
new file mode 100644
index 0000000000..614c7d4dbb
--- /dev/null
+++ b/src/client/pages/instance/files-settings.vue
@@ -0,0 +1,92 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSwitch v-model:value="cacheRemoteFiles">
+ {{ $ts.cacheRemoteFiles }}
+ <template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
+ </FormSwitch>
+
+ <FormSwitch v-model:value="proxyRemoteFiles">
+ {{ $ts.proxyRemoteFiles }}
+ <template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
+ </FormSwitch>
+
+ <FormInput v-model:value="localDriveCapacityMb" type="number">
+ <span>{{ $ts.driveCapacityPerLocalAccount }}</span>
+ <template #suffix>MB</template>
+ <template #desc>{{ $ts.inMb }}</template>
+ </FormInput>
+
+ <FormInput v-model:value="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
+ <span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
+ <template #suffix>MB</template>
+ <template #desc>{{ $ts.inMb }}</template>
+ </FormInput>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.files,
+ icon: 'fas fa-cloud'
+ },
+ cacheRemoteFiles: false,
+ proxyRemoteFiles: false,
+ localDriveCapacityMb: 0,
+ remoteDriveCapacityMb: 0,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.cacheRemoteFiles = meta.cacheRemoteFiles;
+ this.proxyRemoteFiles = meta.proxyRemoteFiles;
+ this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
+ this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
+ },
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ cacheRemoteFiles: this.cacheRemoteFiles,
+ proxyRemoteFiles: this.proxyRemoteFiles,
+ localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
+ remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue
index ed46dd466c..427c5b411a 100644
--- a/src/client/pages/instance/files.vue
+++ b/src/client/pages/instance/files.vue
@@ -80,6 +80,8 @@ export default defineComponent({
MkDriveFileThumbnail,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
@@ -114,6 +116,10 @@ export default defineComponent({
},
},
+ mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
methods: {
clear() {
os.dialog({
@@ -153,6 +159,8 @@ export default defineComponent({
<style lang="scss" scoped>
.xrmjdkdw {
+ margin: var(--margin);
+
.urempief {
margin-top: var(--margin);
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index f0240718aa..10406f339b 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -1,171 +1,239 @@
<template>
-<div v-if="meta" v-show="page === 'index'" class="xhexznfu _section">
- <MkFolder>
- <template #header><i class="fas fa-tachometer-alt"></i> {{ $ts.overview }}</template>
-
- <div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }">
- <MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/>
-
- <MkContainer :foldable="true" class="_gap">
- <template #header><i class="fas fa-info-circle"></i>{{ $ts.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>
- </MkContainer>
-
- <MkContainer :foldable="true" :scrollable="true" class="_gap" style="height: 300px;">
- <template #header><i class="fas fa-database"></i>{{ $ts.database }}</template>
-
- <div class="_content" v-if="dbInfo">
- <table style="border-collapse: collapse; width: 100%;">
- <tr style="opacity: 0.7;">
- <th style="text-align: left; padding: 0 8px 8px 0;">Table</th>
- <th style="text-align: left; padding: 0 8px 8px 0;">Records</th>
- <th style="text-align: left; padding: 0 0 8px 0;">Size</th>
- </tr>
- <tr v-for="table in dbInfo" :key="table[0]">
- <th style="text-align: left; padding: 0 8px 0 0; word-break: break-all;">{{ table[0] }}</th>
- <td style="padding: 0 8px 0 0;">{{ number(table[1].count) }}</td>
- <td style="padding: 0; opacity: 0.7;">{{ bytes(table[1].size) }}</td>
- </tr>
- </table>
+<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el">
+ <div class="nav" v-if="!narrow || page == null">
+ <FormBase>
+ <FormGroup>
+ <div class="_formItem">
+ <div class="_formPanel lxpfedzu">
+ <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
+ </div>
</div>
- </MkContainer>
- </div>
- </MkFolder>
-</div>
-<div v-if="page === 'logs'" class="_section">
- <MkFolder>
- <template #header><i class="fas fa-stream"></i> {{ $ts.logs }}</template>
-
- <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>
- </MkFolder>
-</div>
-<div v-if="page === 'metrics'">
- <XMetrics/>
+ <FormLink :active="page === 'overview'" replace to="/instance/overview"><template #icon><i class="fas fa-tachometer-alt"></i></template>{{ $ts.overview }}</FormLink>
+ </FormGroup>
+ <FormGroup>
+ <template #label>{{ $ts.quickAction }}</template>
+ <FormButton @click="lookup"><i class="fas fa-search"></i> {{ $ts.lookup }}</FormButton>
+ <FormButton v-if="$instance.disableRegistration" @click="invite"><i class="fas fa-user"></i> {{ $ts.invite }}</FormButton>
+ </FormGroup>
+ <FormGroup>
+ <FormLink :active="page === 'users'" replace to="/instance/users"><template #icon><i class="fas fa-users"></i></template>{{ $ts.users }}</FormLink>
+ <FormLink :active="page === 'emojis'" replace to="/instance/emojis"><template #icon><i class="fas fa-laugh"></i></template>{{ $ts.customEmojis }}</FormLink>
+ <FormLink :active="page === 'federation'" replace to="/instance/federation"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.federation }}</FormLink>
+ <FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
+ <FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
+ <FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
+ <FormLink :active="page === 'database'" replace to="/instance/database"><template #icon><i class="fas fa-database"></i></template>{{ $ts.database }}</FormLink>
+ <FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
+ </FormGroup>
+ <FormGroup>
+ <template #label>{{ $ts.settings }}</template>
+ <FormLink :active="page === 'settings'" replace to="/instance/settings"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.general }}</FormLink>
+ <FormLink :active="page === 'files-settings'" replace to="/instance/files-settings"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
+ <FormLink :active="page === 'email-settings'" replace to="/instance/email-settings"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.emailServer }}</FormLink>
+ <FormLink :active="page === 'object-storage'" replace to="/instance/object-storage"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.objectStorage }}</FormLink>
+ <FormLink :active="page === 'security'" replace to="/instance/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</FormLink>
+ <FormLink :active="page === 'service-worker'" replace to="/instance/service-worker"><template #icon><i class="fas fa-bolt"></i></template>ServiceWorker</FormLink>
+ <FormLink :active="page === 'relays'" replace to="/instance/relays"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.relays }}</FormLink>
+ <FormLink :active="page === 'integrations'" replace to="/instance/integrations"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</FormLink>
+ <FormLink :active="page === 'instance-block'" replace to="/instance/instance-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.instanceBlocking }}</FormLink>
+ <FormLink :active="page === 'proxy-account'" replace to="/instance/proxy-account"><template #icon><i class="fas fa-ghost"></i></template>{{ $ts.proxyAccount }}</FormLink>
+ <FormLink :active="page === 'other-settings'" replace to="/instance/other-settings"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.other }}</FormLink>
+ </FormGroup>
+ </FormBase>
+ </div>
+ <div class="main">
+ <component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
+ </div>
</div>
</template>
<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import VueJsonPretty from 'vue-json-pretty';
-import MkInstanceStats from '@client/components/instance-stats.vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkContainer from '@client/components/ui/container.vue';
-import MkFolder from '@client/components/ui/folder.vue';
-import { version, url } from '@client/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 '@client/os';
+import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
+import { i18n } from '@client/i18n';
+import FormLink from '@client/components/form/link.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormButton from '@client/components/form/button.vue';
+import { scroll } from '@client/scripts/scroll';
import * as symbols from '@client/symbols';
+import * as os from '@client/os';
+import { lookupUser } from '@client/scripts/lookup-user';
export default defineComponent({
components: {
- MkInstanceStats,
- MkButton,
- MkSelect,
- MkInput,
- MkContainer,
- MkFolder,
- XMetrics,
- VueJsonPretty,
+ FormBase,
+ FormLink,
+ FormGroup,
+ FormButton,
},
- data() {
- return {
- [symbols.PAGE_INFO]: {
- tabs: [{
- id: 'index',
- title: null,
- tooltip: this.$ts.instance,
- icon: 'fas fa-server',
- onClick: () => { this.page = 'index'; },
- selected: computed(() => this.page === 'index')
- }, {
- id: 'metrics',
- title: null,
- tooltip: this.$ts.metrics,
- icon: 'fas fa-heartbeat',
- onClick: () => { this.page = 'metrics'; },
- selected: computed(() => this.page === 'metrics')
- }, {
- id: 'logs',
- title: null,
- tooltip: this.$ts.logs,
- icon: 'fas fa-stream',
- onClick: () => { this.page = 'logs'; },
- selected: computed(() => this.page === 'logs')
- }]
- },
- page: 'index',
- version,
- url,
- stats: null,
- serverInfo: null,
- modLogs: [],
- dbInfo: null,
+ props: {
+ initialPage: {
+ type: String,
+ required: false
}
},
- computed: {
- meta() {
- return this.$instance;
- },
- },
+ setup(props, context) {
+ const indexInfo = {
+ title: i18n.locale.instance,
+ icon: 'fas fa-cog'
+ };
+ const INFO = ref(indexInfo);
+ const page = ref(props.initialPage);
+ const narrow = ref(false);
+ const view = ref(null);
+ const el = ref(null);
+ const onInfo = (viewInfo) => {
+ INFO.value = viewInfo;
+ };
+ const pageProps = ref({});
+ const component = computed(() => {
+ if (page.value == null) return null;
+ switch (page.value) {
+ case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
+ case 'users': return defineAsyncComponent(() => import('./users.vue'));
+ case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
+ case 'federation': return defineAsyncComponent(() => import('./federation.vue'));
+ case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
+ case 'files': return defineAsyncComponent(() => import('./files.vue'));
+ case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
+ case 'database': return defineAsyncComponent(() => import('./database.vue'));
+ case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
+ case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
+ case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
+ case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
+ case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
+ case 'security': return defineAsyncComponent(() => import('./security.vue'));
+ case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue'));
+ case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
+ case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
+ case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
+ case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue'));
+ case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue'));
+ case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue'));
+ case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
+ case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
+ case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
+ }
+ });
- mounted() {
- this.fetchJobs();
- this.fetchModLogs();
+ watch(component, () => {
+ pageProps.value = {};
- os.api('admin/server-info', {}).then(res => {
- this.serverInfo = res;
- });
+ nextTick(() => {
+ scroll(el.value, 0);
+ });
+ }, { immediate: true });
- os.api('admin/get-table-stats', {}).then(res => {
- this.dbInfo = Object.entries(res).sort((a, b) => b[1].size - a[1].size);
+ watch(() => props.initialPage, () => {
+ if (props.initialPage == null && !narrow.value) {
+ page.value = 'overview';
+ } else {
+ page.value = props.initialPage;
+ if (props.initialPage == null) {
+ INFO.value = indexInfo;
+ }
+ }
});
- },
- methods: {
- async showInstanceInfo(q) {
- let instance = q;
- if (typeof q === 'string') {
- instance = await os.api('federation/show-instance', {
- host: q
- });
+ onMounted(() => {
+ narrow.value = el.value.offsetWidth < 800;
+ if (!narrow.value) {
+ page.value = 'overview';
}
- os.popup(MkInstanceInfo, {
- instance: instance
- }, {}, 'closed');
- },
-
- fetchJobs() {
- os.api('admin/queue/deliver-delayed', {}).then(jobs => {
- this.jobs = jobs;
- });
- },
+ });
- fetchModLogs() {
- os.api('admin/show-moderation-logs', {}).then(logs => {
- this.modLogs = logs;
+ const invite = () => {
+ os.api('admin/invite').then(x => {
+ os.dialog({
+ type: 'info',
+ text: x.code
+ });
+ }).catch(e => {
+ os.dialog({
+ type: 'error',
+ text: e
+ });
});
- },
+ };
- bytes,
+ const lookup = (ev) => {
+ os.modalMenu([{
+ text: i18n.locale.user,
+ icon: 'fas fa-user',
+ action: () => {
+ lookupUser();
+ }
+ }, {
+ text: i18n.locale.note,
+ icon: 'fas fa-pencil-alt',
+ action: () => {
+ alert('TODO');
+ }
+ }, {
+ text: i18n.locale.file,
+ icon: 'fas fa-cloud',
+ action: () => {
+ alert('TODO');
+ }
+ }, {
+ text: i18n.locale.instance,
+ icon: 'fas fa-globe',
+ action: () => {
+ alert('TODO');
+ }
+ }], ev.currentTarget || ev.target);
+ };
- number,
- }
+ return {
+ [symbols.PAGE_INFO]: INFO,
+ page,
+ narrow,
+ view,
+ el,
+ onInfo,
+ pageProps,
+ component,
+ invite,
+ lookup,
+ };
+ },
});
</script>
+
+<style lang="scss" scoped>
+.hiyeyicy {
+ &.wide {
+ display: flex;
+ max-width: 1100px;
+ margin: 0 auto;
+ height: 100%;
+
+ > .nav {
+ width: 32%;
+ box-sizing: border-box;
+ border-right: solid 0.5px var(--divider);
+ overflow: auto;
+ }
+
+ > .main {
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
+ --baseContentWidth: 100%;
+ }
+ }
+}
+
+.lxpfedzu {
+ padding: 16px;
+
+ > img {
+ display: block;
+ margin: auto;
+ height: 42px;
+ border-radius: 8px;
+ }
+}
+</style>
diff --git a/src/client/pages/instance/instance-block.vue b/src/client/pages/instance/instance-block.vue
new file mode 100644
index 0000000000..ed5740f339
--- /dev/null
+++ b/src/client/pages/instance/instance-block.vue
@@ -0,0 +1,71 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormTextarea v-model:value="blockedHosts">
+ <span>{{ $ts.blockedInstances }}</span>
+ <template #desc>{{ $ts.blockedInstancesDescription }}</template>
+ </FormTextarea>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormTextarea from '@client/components/form/textarea.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormTextarea,
+ FormInfo,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.instanceBlocking,
+ icon: 'fas fa-ban'
+ },
+ blockedHosts: '',
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.blockedHosts = meta.blockedHosts.join('\n');
+ },
+
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ blockedHosts: this.blockedHosts.split('\n') || [],
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/integrations-discord.vue b/src/client/pages/instance/integrations-discord.vue
new file mode 100644
index 0000000000..c7508918f8
--- /dev/null
+++ b/src/client/pages/instance/integrations-discord.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSwitch v-model:value="enableDiscordIntegration">
+ {{ $ts.enable }}
+ </FormSwitch>
+
+ <template v-if="enableDiscordIntegration">
+ <FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
+
+ <FormInput v-model:value="discordClientId">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Client ID
+ </FormInput>
+
+ <FormInput v-model:value="discordClientSecret">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Client Secret
+ </FormInput>
+ </template>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormInfo,
+ FormButton,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: 'Discord',
+ icon: 'fab fa-discord'
+ },
+ enableDiscordIntegration: false,
+ discordClientId: null,
+ discordClientSecret: null,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableDiscordIntegration = meta.enableDiscordIntegration;
+ this.discordClientId = meta.discordClientId;
+ this.discordClientSecret = meta.discordClientSecret;
+ },
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ enableDiscordIntegration: this.enableDiscordIntegration,
+ discordClientId: this.discordClientId,
+ discordClientSecret: this.discordClientSecret,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/integrations-github.vue b/src/client/pages/instance/integrations-github.vue
new file mode 100644
index 0000000000..16586b15b4
--- /dev/null
+++ b/src/client/pages/instance/integrations-github.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSwitch v-model:value="enableGithubIntegration">
+ {{ $ts.enable }}
+ </FormSwitch>
+
+ <template v-if="enableGithubIntegration">
+ <FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
+
+ <FormInput v-model:value="githubClientId">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Client ID
+ </FormInput>
+
+ <FormInput v-model:value="githubClientSecret">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Client Secret
+ </FormInput>
+ </template>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormInfo,
+ FormButton,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: 'GitHub',
+ icon: 'fab fa-github'
+ },
+ enableGithubIntegration: false,
+ githubClientId: null,
+ githubClientSecret: null,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableGithubIntegration = meta.enableGithubIntegration;
+ this.githubClientId = meta.githubClientId;
+ this.githubClientSecret = meta.githubClientSecret;
+ },
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ enableGithubIntegration: this.enableGithubIntegration,
+ githubClientId: this.githubClientId,
+ githubClientSecret: this.githubClientSecret,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/integrations-twitter.vue b/src/client/pages/instance/integrations-twitter.vue
new file mode 100644
index 0000000000..b08b7f40a5
--- /dev/null
+++ b/src/client/pages/instance/integrations-twitter.vue
@@ -0,0 +1,85 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSwitch v-model:value="enableTwitterIntegration">
+ {{ $ts.enable }}
+ </FormSwitch>
+
+ <template v-if="enableTwitterIntegration">
+ <FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
+
+ <FormInput v-model:value="twitterConsumerKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Consumer Key
+ </FormInput>
+
+ <FormInput v-model:value="twitterConsumerSecret">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Consumer Secret
+ </FormInput>
+ </template>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormInfo,
+ FormButton,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: 'Twitter',
+ icon: 'fab fa-twitter'
+ },
+ enableTwitterIntegration: false,
+ twitterConsumerKey: null,
+ twitterConsumerSecret: null,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableTwitterIntegration = meta.enableTwitterIntegration;
+ this.twitterConsumerKey = meta.twitterConsumerKey;
+ this.twitterConsumerSecret = meta.twitterConsumerSecret;
+ },
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ enableTwitterIntegration: this.enableTwitterIntegration,
+ twitterConsumerKey: this.twitterConsumerKey,
+ twitterConsumerSecret: this.twitterConsumerSecret,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/integrations.vue b/src/client/pages/instance/integrations.vue
new file mode 100644
index 0000000000..7debedc367
--- /dev/null
+++ b/src/client/pages/instance/integrations.vue
@@ -0,0 +1,73 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormLink to="/instance/integrations/twitter">
+ <i class="fab fa-twitter"></i> Twitter
+ <template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
+ </FormLink>
+ <FormLink to="/instance/integrations/github">
+ <i class="fab fa-github"></i> GitHub
+ <template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
+ </FormLink>
+ <FormLink to="/instance/integrations/discord">
+ <i class="fab fa-discord"></i> Discord
+ <template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
+ </FormLink>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormLink from '@client/components/form/link.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormTextarea from '@client/components/form/textarea.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormLink,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormTextarea,
+ FormInfo,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.integration,
+ icon: 'fas fa-share-alt'
+ },
+ enableTwitterIntegration: false,
+ enableGithubIntegration: false,
+ enableDiscordIntegration: false,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableTwitterIntegration = meta.enableTwitterIntegration;
+ this.enableGithubIntegration = meta.enableGithubIntegration;
+ this.enableDiscordIntegration = meta.enableDiscordIntegration;
+ },
+ }
+});
+</script>
diff --git a/src/client/pages/instance/index.metrics.vue b/src/client/pages/instance/metrics.vue
index 9dd115240e..18cfe5eee2 100644
--- a/src/client/pages/instance/index.metrics.vue
+++ b/src/client/pages/instance/metrics.vue
@@ -1,101 +1,52 @@
<template>
-<div>
- <MkFolder>
- <template #header><i class="fas fa-heartbeat"></i> {{ $ts.metrics }}</template>
- <div class="_section" style="padding: 0 var(--margin);">
- <div class="_content">
- <MkContainer :foldable="false" class="_gap">
- <template #header><i class="fas fa-microchip"></i>{{ $ts.cpuAndMemory }}</template>
- <!--
- <template #func>
- <button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button>
- <button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></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 :foldable="false" class="_gap">
- <template #header><i class="fas fa-hdd"></i> {{ $ts.disk }}</template>
- <!--
- <template #func>
- <button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button>
- <button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></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 :foldable="false" class="_gap">
- <template #header><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</template>
- <!--
- <template #func>
- <button class="_button" @click="resume" :disabled="!paused"><i class="fas fa-play"></i></button>
- <button class="_button" @click="pause" :disabled="paused"><i class="fas fa-pause"></i></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 class="_formItem">
+ <div class="_formLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
+ <div class="_formPanel xhexznfu">
+ <div>
+ <canvas :ref="cpumem"></canvas>
+ </div>
+ <div 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>
- </MkFolder>
-
- <MkFolder>
- <template #header><i class="fas fa-clipboard-list"></i> {{ $ts.jobQueue }}</template>
-
- <div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }">
- <MkContainer :foldable="false" :scrollable="true" :resize-base-el="() => $el">
- <template #header><i class="fas fa-exclamation-triangle"></i> {{ $ts.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>
+</div>
+<div class="_formItem">
+ <div class="_formLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
+ <div class="_formPanel xhexznfu">
+ <div>
+ <canvas :ref="disk"></canvas>
+ </div>
+ <div 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>
- </MkContainer>
- <XQueue :connection="queueConnection" domain="inbox" ref="queue" class="queue">
- <template #title><i class="fas fa-exchange-alt"></i> In</template>
- </XQueue>
- <XQueue :connection="queueConnection" domain="deliver" class="queue">
- <template #title><i class="fas fa-exchange-alt"></i> Out</template>
- </XQueue>
+ </div>
+ </div>
+ </div>
+</div>
+<div class="_formItem">
+ <div class="_formLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
+ <div class="_formPanel xhexznfu">
+ <div>
+ <canvas :ref="net"></canvas>
+ </div>
+ <div v-if="serverInfo">
+ <div class="_table">
+ <div class="_row">
+ <div class="_cell"><div class="_label">Interface</div>{{ serverInfo.net.interface }}</div>
+ </div>
+ </div>
</div>
- </MkFolder>
+ </div>
</div>
</template>
@@ -188,9 +139,11 @@ export default defineComponent({
},
beforeUnmount() {
- this.connection.off('stats', this.onStats);
- this.connection.off('statsLog', this.onStatsLog);
- this.connection.dispose();
+ if (this.connection) {
+ this.connection.off('stats', this.onStats);
+ this.connection.off('statsLog', this.onStatsLog);
+ this.connection.dispose();
+ }
this.queueConnection.dispose();
},
@@ -232,9 +185,9 @@ export default defineComponent({
aspectRatio: 3,
layout: {
padding: {
- left: 0,
- right: 0,
- top: 8,
+ left: 16,
+ right: 16,
+ top: 16,
bottom: 0
}
},
@@ -304,9 +257,9 @@ export default defineComponent({
aspectRatio: 3,
layout: {
padding: {
- left: 0,
- right: 0,
- top: 8,
+ left: 16,
+ right: 16,
+ top: 16,
bottom: 0
}
},
@@ -375,9 +328,9 @@ export default defineComponent({
aspectRatio: 3,
layout: {
padding: {
- left: 0,
- right: 0,
- top: 8,
+ left: 16,
+ right: 16,
+ top: 16,
bottom: 0
}
},
@@ -494,81 +447,9 @@ export default defineComponent({
<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);
- }
+ > div:nth-child(2) {
+ padding: 16px;
+ border-top: solid 0.5px var(--divider);
}
}
</style>
diff --git a/src/client/pages/instance/object-storage.vue b/src/client/pages/instance/object-storage.vue
new file mode 100644
index 0000000000..814aeb6e48
--- /dev/null
+++ b/src/client/pages/instance/object-storage.vue
@@ -0,0 +1,154 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSwitch v-model:value="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
+
+ <template v-if="useObjectStorage">
+ <FormInput v-model:value="objectStorageBaseUrl">
+ <span>{{ $ts.objectStorageBaseUrl }}</span>
+ <template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
+ </FormInput>
+
+ <FormInput v-model:value="objectStorageBucket">
+ <span>{{ $ts.objectStorageBucket }}</span>
+ <template #desc>{{ $ts.objectStorageBucketDesc }}</template>
+ </FormInput>
+
+ <FormInput v-model:value="objectStoragePrefix">
+ <span>{{ $ts.objectStoragePrefix }}</span>
+ <template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
+ </FormInput>
+
+ <FormInput v-model:value="objectStorageEndpoint">
+ <span>{{ $ts.objectStorageEndpoint }}</span>
+ <template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
+ </FormInput>
+
+ <FormInput v-model:value="objectStorageRegion">
+ <span>{{ $ts.objectStorageRegion }}</span>
+ <template #desc>{{ $ts.objectStorageRegionDesc }}</template>
+ </FormInput>
+
+ <FormInput v-model:value="objectStorageAccessKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <span>Access key</span>
+ </FormInput>
+
+ <FormInput v-model:value="objectStorageSecretKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <span>Secret key</span>
+ </FormInput>
+
+ <FormSwitch v-model:value="objectStorageUseSSL">
+ {{ $ts.objectStorageUseSSL }}
+ <template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
+ </FormSwitch>
+
+ <FormSwitch v-model:value="objectStorageUseProxy">
+ {{ $ts.objectStorageUseProxy }}
+ <template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
+ </FormSwitch>
+
+ <FormSwitch v-model:value="objectStorageSetPublicRead">
+ {{ $ts.objectStorageSetPublicRead }}
+ </FormSwitch>
+
+ <FormSwitch v-model:value="objectStorageS3ForcePathStyle">
+ s3ForcePathStyle
+ </FormSwitch>
+ </template>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.objectStorage,
+ icon: 'fas fa-cloud'
+ },
+ useObjectStorage: false,
+ objectStorageBaseUrl: null,
+ objectStorageBucket: null,
+ objectStoragePrefix: null,
+ objectStorageEndpoint: null,
+ objectStorageRegion: null,
+ objectStoragePort: null,
+ objectStorageAccessKey: null,
+ objectStorageSecretKey: null,
+ objectStorageUseSSL: false,
+ objectStorageUseProxy: false,
+ objectStorageSetPublicRead: false,
+ objectStorageS3ForcePathStyle: true,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.useObjectStorage = meta.useObjectStorage;
+ this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
+ this.objectStorageBucket = meta.objectStorageBucket;
+ this.objectStoragePrefix = meta.objectStoragePrefix;
+ this.objectStorageEndpoint = meta.objectStorageEndpoint;
+ this.objectStorageRegion = meta.objectStorageRegion;
+ this.objectStoragePort = meta.objectStoragePort;
+ this.objectStorageAccessKey = meta.objectStorageAccessKey;
+ this.objectStorageSecretKey = meta.objectStorageSecretKey;
+ this.objectStorageUseSSL = meta.objectStorageUseSSL;
+ this.objectStorageUseProxy = meta.objectStorageUseProxy;
+ this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
+ this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
+ },
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ useObjectStorage: this.useObjectStorage,
+ objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
+ objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
+ objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
+ objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
+ objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
+ objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
+ objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
+ objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
+ objectStorageUseSSL: this.objectStorageUseSSL,
+ objectStorageUseProxy: this.objectStorageUseProxy,
+ objectStorageSetPublicRead: this.objectStorageSetPublicRead,
+ objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/instance/other-settings.vue
new file mode 100644
index 0000000000..b3954149a8
--- /dev/null
+++ b/src/client/pages/instance/other-settings.vue
@@ -0,0 +1,68 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormGroup>
+ <FormInput v-model:value="summalyProxy">
+ <template #prefix><i class="fas fa-link"></i></template>
+ Summaly Proxy URL
+ </FormInput>
+ </FormGroup>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.other,
+ icon: 'fas fa-cogs'
+ },
+ summalyProxy: '',
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.summalyProxy = meta.summalyProxy;
+ },
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ summalyProxy: this.summalyProxy,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/overview.vue b/src/client/pages/instance/overview.vue
new file mode 100644
index 0000000000..651ace08f9
--- /dev/null
+++ b/src/client/pages/instance/overview.vue
@@ -0,0 +1,131 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSuspense :p="fetchStats" v-slot="{ result: stats }">
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>Users</template>
+ <template #value>{{ number(stats.originalUsersCount) }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>Notes</template>
+ <template #value>{{ number(stats.originalNotesCount) }}</template>
+ </FormKeyValueView>
+ </FormGroup>
+ </FormSuspense>
+
+ <div class="_formItem">
+ <div class="_formPanel">
+ <MkInstanceStats :chart-limit="300" :detailed="true"/>
+ </div>
+ </div>
+
+ <XMetrics/>
+
+ <FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>Node.js</template>
+ <template #value>{{ serverInfo.node }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>PostgreSQL</template>
+ <template #value>{{ serverInfo.psql }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>Redis</template>
+ <template #value>{{ serverInfo.redis }}</template>
+ </FormKeyValueView>
+ </FormGroup>
+ </FormSuspense>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw } from 'vue';
+import VueJsonPretty from 'vue-json-pretty';
+import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormTextarea from '@client/components/form/textarea.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import MkInstanceStats from '@client/components/instance-stats.vue';
+import MkButton from '@client/components/ui/button.vue';
+import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/ui/input.vue';
+import MkContainer from '@client/components/ui/container.vue';
+import MkFolder from '@client/components/ui/folder.vue';
+import { version, url } from '@client/config';
+import bytes from '../../filters/bytes';
+import number from '../../filters/number';
+import MkInstanceInfo from './instance.vue';
+import XMetrics from './metrics.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+ components: {
+ FormBase,
+ FormSuspense,
+ FormGroup,
+ FormKeyValueView,
+ MkInstanceStats,
+ MkButton,
+ MkSelect,
+ MkInput,
+ MkContainer,
+ MkFolder,
+ XMetrics,
+ VueJsonPretty,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.overview,
+ icon: 'fas fa-tachometer-alt'
+ },
+ page: 'index',
+ version,
+ url,
+ stats: null,
+ fetchStats: () => os.api('stats', {}),
+ fetchServerInfo: () => os.api('admin/server-info', {}),
+ fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
+ fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ this.meta = await os.api('meta', { detail: true });
+ },
+
+ 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');
+ },
+
+ bytes,
+
+ number,
+ }
+});
+</script>
diff --git a/src/client/pages/instance/proxy-account.vue b/src/client/pages/instance/proxy-account.vue
new file mode 100644
index 0000000000..3e2df8dcb4
--- /dev/null
+++ b/src/client/pages/instance/proxy-account.vue
@@ -0,0 +1,86 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>{{ $ts.proxyAccount }}</template>
+ <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
+ </FormKeyValueView>
+ <template #caption>{{ $ts.proxyAccountDescription }}</template>
+ </FormGroup>
+
+ <FormButton @click="chooseProxyAccount" primary>{{ $ts.selectAccount }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormTextarea from '@client/components/form/textarea.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormKeyValueView,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormTextarea,
+ FormInfo,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.proxyAccount,
+ icon: 'fas fa-ghost'
+ },
+ proxyAccount: null,
+ proxyAccountId: null,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.proxyAccountId = meta.proxyAccountId;
+ if (this.proxyAccountId) {
+ this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
+ }
+ },
+
+ chooseProxyAccount() {
+ os.selectUser().then(user => {
+ this.proxyAccount = user;
+ this.proxyAccountId = user.id;
+ this.save();
+ });
+ },
+
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ proxyAccountId: this.proxyAccountId,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
index 0eb70debfb..446c979209 100644
--- a/src/client/pages/instance/queue.chart.vue
+++ b/src/client/pages/instance/queue.chart.vue
@@ -1,27 +1,29 @@
<template>
-<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>{{ 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 class="_formItem">
+ <div class="_formLabel"><slot name="title"></slot></div>
+ <div class="_formPanel pumxzjhg">
+ <div class="_table status">
+ <div class="_row">
+ <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
+ <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
+ <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
+ <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
+ </div>
</div>
- </div>
- <div class="_content" style="margin-bottom: -8px;">
- <canvas ref="chart"></canvas>
- </div>
- <div class="_content" style="max-height: 180px; overflow: auto;">
- <div v-if="jobs.length > 0">
- <div v-for="job in jobs" :key="job[0]">
- <span>{{ job[0] }}</span>
- <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
+ <div class="">
+ <canvas ref="chart"></canvas>
+ </div>
+ <div class="jobs">
+ <div v-if="jobs.length > 0">
+ <div v-for="job in jobs" :key="job[0]">
+ <span>{{ job[0] }}</span>
+ <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
+ </div>
</div>
+ <span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
</div>
- <span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
</div>
-</section>
+</div>
</template>
<script lang="ts">
@@ -110,10 +112,10 @@ export default defineComponent({
aspectRatio: 3,
layout: {
padding: {
- left: 0,
- right: 0,
- top: 8,
- bottom: 0
+ left: 16,
+ right: 16,
+ top: 16,
+ bottom: 12
}
},
legend: {
@@ -198,3 +200,19 @@ export default defineComponent({
}
});
</script>
+
+<style lang="scss" scoped>
+.pumxzjhg {
+ > .status {
+ padding: 16px;
+ border-bottom: solid 0.5px var(--divider);
+ }
+
+ > .jobs {
+ padding: 16px;
+ border-top: solid 0.5px var(--divider);
+ max-height: 180px;
+ overflow: auto;
+ }
+}
+</style>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue
index 0c1e0e51b5..2dccf48d31 100644
--- a/src/client/pages/instance/queue.vue
+++ b/src/client/pages/instance/queue.vue
@@ -1,43 +1,47 @@
<template>
-<div>
+<FormBase>
<XQueue :connection="connection" domain="inbox">
- <template #title><i class="fas fa-exchange-alt"></i> In</template>
+ <template #title>In</template>
</XQueue>
<XQueue :connection="connection" domain="deliver">
- <template #title><i class="fas fa-exchange-alt"></i> Out</template>
+ <template #title>Out</template>
</XQueue>
- <section class="_section">
- <div class="_content">
- <MkButton @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton>
- </div>
- </section>
-</div>
+ <FormButton @click="clear()" danger><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import XQueue from './queue.chart.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormButton from '@client/components/form/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
+ FormBase,
+ FormButton,
MkButton,
XQueue,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.jobQueue,
- icon: 'fas fa-exchange-alt',
+ icon: 'fas fa-clipboard-list',
},
connection: os.stream.useSharedConnection('queueStats'),
}
},
mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+
this.$nextTick(() => {
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue
index dbf75ea53b..a3e4e7d1da 100644
--- a/src/client/pages/instance/relays.vue
+++ b/src/client/pages/instance/relays.vue
@@ -1,44 +1,41 @@
<template>
-<div class="relaycxt">
- <section class="_section add">
- <div class="_title"><i class="fas fa-plus"></i> {{ $ts.addRelay }}</div>
- <div class="_content">
- <MkInput v-model:value="inbox">
- <span>{{ $ts.inboxUrl }}</span>
- </MkInput>
- <MkButton @click="add(inbox)" primary><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
- </div>
- </section>
+<FormBase class="relaycxt">
+ <FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
- <section class="_section relays">
- <div class="_title"><i class="fas fa-project-diagram"></i> {{ $ts.addedRelays }}</div>
- <div class="_content relay" v-for="relay in relays" :key="relay.inbox">
+ <div class="_formItem" v-for="relay in relays" :key="relay.inbox">
+ <div class="_formPanel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
- <MkButton class="button" inline @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+ <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
</div>
- </section>
-</div>
+ </div>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormButton from '@client/components/form/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
+ FormBase,
+ FormButton,
MkButton,
MkInput,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.relays,
- icon: 'fas fa-project-diagram',
+ icon: 'fas fa-globe',
},
relays: [],
inbox: '',
@@ -49,8 +46,19 @@ export default defineComponent({
this.refresh();
},
+ mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
methods: {
- add(inbox: string) {
+ async addRelay() {
+ const { canceled, result: inbox } = await os.dialog({
+ title: this.$ts.addRelay,
+ input: {
+ placeholder: this.$ts.inboxUrl
+ }
+ });
+ if (canceled) return;
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
@@ -86,9 +94,5 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-._content.relay {
- div {
- margin: 0.5em 0;
- }
-}
+
</style>
diff --git a/src/client/pages/instance/security.vue b/src/client/pages/instance/security.vue
new file mode 100644
index 0000000000..e3397a113b
--- /dev/null
+++ b/src/client/pages/instance/security.vue
@@ -0,0 +1,77 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormLink to="/instance/bot-protection">
+ <i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
+ <template #suffix v-if="enableHcaptcha">hCaptcha</template>
+ <template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
+ <template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
+ </FormLink>
+
+ <FormSwitch v-model:value="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import FormLink from '@client/components/form/link.vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormLink,
+ FormSwitch,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormInfo,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.security,
+ icon: 'fas fa-lock'
+ },
+ enableHcaptcha: false,
+ enableRecaptcha: false,
+ enableRegistration: false,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableHcaptcha = meta.enableHcaptcha;
+ this.enableRecaptcha = meta.enableRecaptcha;
+ this.enableRegistration = !meta.disableRegistration;
+ },
+
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ disableRegistration: !this.enableRegistration,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/service-worker.vue b/src/client/pages/instance/service-worker.vue
new file mode 100644
index 0000000000..a52932bb75
--- /dev/null
+++ b/src/client/pages/instance/service-worker.vue
@@ -0,0 +1,84 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormSwitch v-model:value="enableServiceWorker">
+ {{ $ts.enableServiceworker }}
+ <template #desc>{{ $ts.serviceworkerInfo }}</template>
+ </FormSwitch>
+
+ <template v-if="enableServiceWorker">
+ <FormInput v-model:value="swPublicKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Public key
+ </FormInput>
+
+ <FormInput v-model:value="swPrivateKey">
+ <template #prefix><i class="fas fa-key"></i></template>
+ Private key
+ </FormInput>
+ </template>
+
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
+
+export default defineComponent({
+ components: {
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormSuspense,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: 'ServiceWorker',
+ icon: 'fas fa-bolt'
+ },
+ enableServiceWorker: false,
+ swPublicKey: null,
+ swPrivateKey: null,
+ }
+ },
+
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
+
+ methods: {
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.enableServiceWorker = meta.enableServiceWorker;
+ this.swPublicKey = meta.swPublickey;
+ this.swPrivateKey = meta.swPrivateKey;
+ },
+ save() {
+ os.apiWithDialog('admin/update-meta', {
+ enableServiceWorker: this.enableServiceWorker,
+ swPublicKey: this.swPublicKey,
+ swPrivateKey: this.swPrivateKey,
+ }).then(() => {
+ fetchInstance();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index b827a77649..66f01c42c7 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -1,581 +1,132 @@
<template>
-<div v-if="meta" class="_section">
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-info-circle"></i> {{ $ts.basicInfo }}</div>
- <div class="_content">
- <MkInput v-model:value="name">{{ $ts.instanceName }}</MkInput>
- <MkTextarea v-model:value="description">{{ $ts.instanceDescription }}</MkTextarea>
- <MkInput v-model:value="iconUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.iconUrl }}</MkInput>
- <MkInput v-model:value="bannerUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.bannerUrl }}</MkInput>
- <MkInput v-model:value="backgroundImageUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.backgroundImageUrl }}</MkInput>
- <MkInput v-model:value="logoImageUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.logoImageUrl }}</MkInput>
- <MkInput v-model:value="tosUrl"><template #icon><i class="fas fa-link"></i></template>{{ $ts.tosUrl }}</MkInput>
- <MkInput v-model:value="maintainerName">{{ $ts.maintainerName }}</MkInput>
- <MkInput v-model:value="maintainerEmail" type="email"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.maintainerEmail }}</MkInput>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
+<FormBase>
+ <FormSuspense :p="init">
+ <FormInput v-model:value="name">
+ <span>{{ $ts.instanceName }}</span>
+ </FormInput>
- <MkInput v-model:value="pinnedClipId">{{ $ts.pinnedClipId }}</MkInput>
+ <FormTextarea v-model:value="description">
+ <span>{{ $ts.instanceDescription }}</span>
+ </FormTextarea>
- <section class="_card _gap">
- <div class="_content">
- <MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()"><template #icon><i class="fas fa-pencil-alt"></i></template>{{ $ts.maxNoteTextLength }}</MkInput>
- </div>
- <div class="_content">
- <MkSwitch v-model:value="enableLocalTimeline" @update:value="save()">{{ $ts.enableLocalTimeline }}</MkSwitch>
- <MkSwitch v-model:value="enableGlobalTimeline" @update:value="save()">{{ $ts.enableGlobalTimeline }}</MkSwitch>
- <MkInfo>{{ $ts.disablingTimelinesInfo }}</MkInfo>
- </div>
- <div class="_content">
- <MkSwitch v-model:value="useStarForReactionFallback" @update:value="save()">{{ $ts.useStarForReactionFallback }}</MkSwitch>
- </div>
- </section>
+ <FormInput v-model:value="iconUrl">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <span>{{ $ts.iconUrl }}</span>
+ </FormInput>
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-user"></i> {{ $ts.registration }}</div>
- <div class="_content">
- <MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $ts.enableRegistration }}</MkSwitch>
- <MkButton v-if="!enableRegistration" @click="invite">{{ $ts.invite }}</MkButton>
- </div>
- </section>
+ <FormInput v-model:value="bannerUrl">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <span>{{ $ts.bannerUrl }}</span>
+ </FormInput>
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-shield-alt"></i> {{ $ts.hcaptcha }}</div>
- <div class="_content">
- <MkSwitch v-model:value="enableHcaptcha">{{ $ts.enableHcaptcha }}</MkSwitch>
- <template v-if="enableHcaptcha">
- <MkInput v-model:value="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.hcaptchaSiteKey }}</MkInput>
- <MkInput v-model:value="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.hcaptchaSecretKey }}</MkInput>
- </template>
- </div>
- <div class="_content" v-if="enableHcaptcha">
- <header>{{ $ts.preview }}</header>
- <captcha v-if="enableHcaptcha" provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
+ <FormInput v-model:value="tosUrl">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <span>{{ $ts.tosUrl }}</span>
+ </FormInput>
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-shield-alt"></i> {{ $ts.recaptcha }}</div>
- <div class="_content">
- <MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $ts.enableRecaptcha }}</MkSwitch>
- <template v-if="enableRecaptcha">
- <MkInput v-model:value="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.recaptchaSiteKey }}</MkInput>
- <MkInput v-model:value="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><i class="fas fa-key"></i></template>{{ $ts.recaptchaSecretKey }}</MkInput>
- </template>
- </div>
- <div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
- <header>{{ $ts.preview }}</header>
- <captcha v-if="enableRecaptcha" provider="grecaptcha" :sitekey="recaptchaSiteKey"/>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
+ <FormInput v-model:value="maintainerName">
+ <span>{{ $ts.maintainerName }}</span>
+ </FormInput>
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-envelope"></i> {{ $ts.emailConfig }}</div>
- <div class="_content">
- <MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></MkSwitch>
- <MkInput v-model:value="email" type="email" :disabled="!enableEmail">{{ $ts.email }}</MkInput>
- <div><b>{{ $ts.smtpConfig }}</b></div>
- <div class="_inputs">
- <MkInput v-model:value="smtpHost" :disabled="!enableEmail">{{ $ts.smtpHost }}</MkInput>
- <MkInput v-model:value="smtpPort" type="number" :disabled="!enableEmail">{{ $ts.smtpPort }}</MkInput>
- </div>
- <div class="_inputs">
- <MkInput v-model:value="smtpUser" :disabled="!enableEmail">{{ $ts.smtpUser }}</MkInput>
- <MkInput v-model:value="smtpPass" type="password" :disabled="!enableEmail">{{ $ts.smtpPass }}</MkInput>
- </div>
- <MkInfo>{{ $ts.emptyToDisableSmtpAuth }}</MkInfo>
- <MkSwitch v-model:value="smtpSecure" :disabled="!enableEmail">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></MkSwitch>
- <div>
- <MkButton :disabled="!enableEmail" primary inline @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- <MkButton :disabled="!enableEmail" inline @click="testEmail()">{{ $ts.testEmail }}</MkButton>
- </div>
- </div>
- </section>
+ <FormInput v-model:value="maintainerEmail" type="email">
+ <template #prefix><i class="fas fa-envelope"></i></template>
+ <span>{{ $ts.maintainerEmail }}</span>
+ </FormInput>
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-bolt"></i> {{ $ts.serviceworker }}</div>
- <div class="_content">
- <MkSwitch v-model:value="enableServiceWorker">{{ $ts.enableServiceworker }}<template #desc>{{ $ts.serviceworkerInfo }}</template></MkSwitch>
- <template v-if="enableServiceWorker">
- <div class="_inputs">
- <MkInput v-model:value="swPublicKey" :disabled="!enableServiceWorker"><template #icon><i class="fas fa-key"></i></template>Public key</MkInput>
- <MkInput v-model:value="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><i class="fas fa-key"></i></template>Private key</MkInput>
- </div>
- </template>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
+ <FormInput v-model:value="maxNoteTextLength" type="number">
+ <template #prefix><i class="fas fa-pencil-alt"></i></template>
+ <span>{{ $ts.maxNoteTextLength }}</span>
+ </FormInput>
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedUsers }}</div>
- <div class="_content">
- <MkTextarea v-model:value="pinnedUsers">
- <template #desc>{{ $ts.pinnedUsersDescription }} <button class="_textButton" @click="addPinUser">{{ $ts.addUser }}</button></template>
- </MkTextarea>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
+ <FormSwitch v-model:value="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
+ <FormSwitch v-model:value="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
+ <FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedPages }}</div>
- <div class="_content">
- <MkTextarea v-model:value="pinnedPages">
- <template #desc>{{ $ts.pinnedPagesDescription }}</template>
- </MkTextarea>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
-
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-cloud"></i> {{ $ts.files }}</div>
- <div class="_content">
- <MkSwitch v-model:value="cacheRemoteFiles">{{ $ts.cacheRemoteFiles }}<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template></MkSwitch>
- <MkSwitch v-model:value="proxyRemoteFiles">{{ $ts.proxyRemoteFiles }}<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template></MkSwitch>
- <MkInput v-model:value="localDriveCapacityMb" type="number">{{ $ts.driveCapacityPerLocalAccount }}<template #suffix>MB</template><template #desc>{{ $ts.inMb }}</template></MkInput>
- <MkInput v-model:value="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $ts.driveCapacityPerRemoteAccount }}<template #suffix>MB</template><template #desc>{{ $ts.inMb }}</template></MkInput>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
-
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-cloud"></i> {{ $ts.objectStorage }}</div>
- <div class="_content">
- <MkSwitch v-model:value="useObjectStorage">{{ $ts.useObjectStorage }}</MkSwitch>
- <template v-if="useObjectStorage">
- <MkInput v-model:value="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $ts.objectStorageBaseUrl }}<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template></MkInput>
- <div class="_inputs">
- <MkInput v-model:value="objectStorageBucket" :disabled="!useObjectStorage">{{ $ts.objectStorageBucket }}<template #desc>{{ $ts.objectStorageBucketDesc }}</template></MkInput>
- <MkInput v-model:value="objectStoragePrefix" :disabled="!useObjectStorage">{{ $ts.objectStoragePrefix }}<template #desc>{{ $ts.objectStoragePrefixDesc }}</template></MkInput>
- </div>
- <MkInput v-model:value="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $ts.objectStorageEndpoint }}<template #desc>{{ $ts.objectStorageEndpointDesc }}</template></MkInput>
- <div class="_inputs">
- <MkInput v-model:value="objectStorageRegion" :disabled="!useObjectStorage">{{ $ts.objectStorageRegion }}<template #desc>{{ $ts.objectStorageRegionDesc }}</template></MkInput>
- </div>
- <div class="_inputs">
- <MkInput v-model:value="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><i class="fas fa-key"></i></template>Access key</MkInput>
- <MkInput v-model:value="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><i class="fas fa-key"></i></template>Secret key</MkInput>
- </div>
- <MkSwitch v-model:value="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $ts.objectStorageUseSSL }}<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template></MkSwitch>
- <MkSwitch v-model:value="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $ts.objectStorageUseProxy }}<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template></MkSwitch>
- <MkSwitch v-model:value="objectStorageSetPublicRead" :disabled="!useObjectStorage">{{ $ts.objectStorageSetPublicRead }}</MkSwitch>
- <MkSwitch v-model:value="objectStorageS3ForcePathStyle" :disabled="!useObjectStorage">s3ForcePathStyle</MkSwitch>
- </template>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
-
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-ghost"></i> {{ $ts.proxyAccount }}</div>
- <div class="_content">
- <MkInput :value="proxyAccount ? proxyAccount.username : null" disabled><template #prefix>@</template>{{ $ts.proxyAccount }}<template #desc>{{ $ts.proxyAccountDescription }}</template></MkInput>
- <MkButton primary @click="chooseProxyAccount">{{ $ts.chooseProxyAccount }}</MkButton>
- </div>
- </section>
-
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-ban"></i> {{ $ts.blockedInstances }}</div>
- <div class="_content">
- <MkTextarea v-model:value="blockedHosts">
- <template #desc>{{ $ts.blockedInstancesDescription }}</template>
- </MkTextarea>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
-
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-share-alt"></i> {{ $ts.integration }}</div>
- <div class="_content">
- <header><i class="fab fa-twitter"></i> Twitter</header>
- <MkSwitch v-model:value="enableTwitterIntegration">{{ $ts.enable }}</MkSwitch>
- <template v-if="enableTwitterIntegration">
- <MkInfo>Callback URL: {{ `${url}/api/tw/cb` }}</MkInfo>
- <MkInput v-model:value="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><i class="fas fa-key"></i></template>Consumer Key</MkInput>
- <MkInput v-model:value="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><i class="fas fa-key"></i></template>Consumer Secret</MkInput>
- </template>
- </div>
- <div class="_content">
- <header><i class="fas fa-github"></i> GitHub</header>
- <MkSwitch v-model:value="enableGithubIntegration">{{ $ts.enable }}</MkSwitch>
- <template v-if="enableGithubIntegration">
- <MkInfo>Callback URL: {{ `${url}/api/gh/cb` }}</MkInfo>
- <MkInput v-model:value="githubClientId" :disabled="!enableGithubIntegration"><template #icon><i class="fas fa-key"></i></template>Client ID</MkInput>
- <MkInput v-model:value="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><i class="fas fa-key"></i></template>Client Secret</MkInput>
- </template>
- </div>
- <div class="_content">
- <header><i class="fas fa-discord"></i> Discord</header>
- <MkSwitch v-model:value="enableDiscordIntegration">{{ $ts.enable }}</MkSwitch>
- <template v-if="enableDiscordIntegration">
- <MkInfo>Callback URL: {{ `${url}/api/dc/cb` }}</MkInfo>
- <MkInput v-model:value="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><i class="fas fa-key"></i></template>Client ID</MkInput>
- <MkInput v-model:value="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><i class="fas fa-key"></i></template>Client Secret</MkInput>
- </template>
- </div>
- <div class="_footer">
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
-
- <section class="_card _gap">
- <div class="_title"><i class="fas fa-archway"></i> Summaly Proxy</div>
- <div class="_content">
- <MkInput v-model:value="summalyProxy">URL</MkInput>
- <MkButton primary @click="save(true)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- </div>
- </section>
-</div>
+ <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </FormSuspense>
+</FormBase>
</template>
<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import MkInfo from '@client/components/ui/info.vue';
-import { url } from '@client/config';
-import getAcct from '@/misc/acct/render';
+import { defineComponent } from 'vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormTextarea from '@client/components/form/textarea.vue';
+import FormInfo from '@client/components/form/info.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
import * as os from '@client/os';
-import { fetchInstance } from '@client/instance';
import * as symbols from '@client/symbols';
+import { fetchInstance } from '@client/instance';
export default defineComponent({
components: {
- MkButton,
- MkInput,
- MkTextarea,
- MkSwitch,
- MkInfo,
- Captcha: defineAsyncComponent(() => import('@client/components/captcha.vue')),
+ FormSwitch,
+ FormInput,
+ FormBase,
+ FormGroup,
+ FormButton,
+ FormTextarea,
+ FormInfo,
+ FormSuspense,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
- title: this.$ts.instance,
- icon: 'fas fa-cog',
+ title: this.$ts.general,
+ icon: 'fas fa-cog'
},
- meta: null,
- url,
- proxyAccount: null,
- proxyAccountId: null,
- cacheRemoteFiles: false,
- proxyRemoteFiles: false,
- localDriveCapacityMb: 0,
- remoteDriveCapacityMb: 0,
- blockedHosts: '',
- pinnedUsers: '',
- pinnedPages: '',
- pinnedClipId: null,
- maintainerName: null,
- maintainerEmail: null,
name: null,
description: null,
tosUrl: null as string | null,
- enableEmail: false,
- email: null,
- bannerUrl: null,
+ maintainerName: null,
+ maintainerEmail: null,
iconUrl: null,
- logoImageUrl: null,
- backgroundImageUrl: null,
+ bannerUrl: null,
maxNoteTextLength: 0,
- enableRegistration: false,
enableLocalTimeline: false,
enableGlobalTimeline: false,
- enableHcaptcha: false,
- hcaptchaSiteKey: null,
- hcaptchaSecretKey: null,
- enableRecaptcha: false,
- recaptchaSiteKey: null,
- recaptchaSecretKey: null,
- enableServiceWorker: false,
- swPublicKey: null,
- swPrivateKey: null,
- useObjectStorage: false,
- objectStorageBaseUrl: null,
- objectStorageBucket: null,
- objectStoragePrefix: null,
- objectStorageEndpoint: null,
- objectStorageRegion: null,
- objectStoragePort: null,
- objectStorageAccessKey: null,
- objectStorageSecretKey: null,
- objectStorageUseSSL: false,
- objectStorageUseProxy: false,
- objectStorageSetPublicRead: false,
- objectStorageS3ForcePathStyle: true,
- enableTwitterIntegration: false,
- twitterConsumerKey: null,
- twitterConsumerSecret: null,
- enableGithubIntegration: false,
- githubClientId: null,
- githubClientSecret: null,
- enableDiscordIntegration: false,
- discordClientId: null,
- discordClientSecret: null,
- useStarForReactionFallback: false,
- smtpSecure: false,
- smtpHost: '',
- smtpPort: 0,
- smtpUser: '',
- smtpPass: '',
- summalyProxy: '',
- }
- },
-
- async created() {
- this.meta = await os.api('meta', { detail: true });
-
- this.name = this.meta.name;
- this.description = this.meta.description;
- this.tosUrl = this.meta.tosUrl;
- this.bannerUrl = this.meta.bannerUrl;
- this.iconUrl = this.meta.iconUrl;
- this.logoImageUrl = this.meta.logoImageUrl;
- this.backgroundImageUrl = this.meta.backgroundImageUrl;
- this.enableEmail = this.meta.enableEmail;
- this.email = this.meta.email;
- this.maintainerName = this.meta.maintainerName;
- this.maintainerEmail = this.meta.maintainerEmail;
- this.maxNoteTextLength = this.meta.maxNoteTextLength;
- this.enableRegistration = !this.meta.disableRegistration;
- this.enableLocalTimeline = !this.meta.disableLocalTimeline;
- this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
- this.enableHcaptcha = this.meta.enableHcaptcha;
- this.hcaptchaSiteKey = this.meta.hcaptchaSiteKey;
- this.hcaptchaSecretKey = this.meta.hcaptchaSecretKey;
- this.enableRecaptcha = this.meta.enableRecaptcha;
- this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
- this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
- this.proxyAccountId = this.meta.proxyAccountId;
- this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
- this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
- this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
- this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
- this.blockedHosts = this.meta.blockedHosts.join('\n');
- this.pinnedUsers = this.meta.pinnedUsers.join('\n');
- this.pinnedPages = this.meta.pinnedPages.join('\n');
- this.pinnedClipId = this.meta.pinnedClipId;
- this.enableServiceWorker = this.meta.enableServiceWorker;
- this.swPublicKey = this.meta.swPublickey;
- this.swPrivateKey = this.meta.swPrivateKey;
- this.useObjectStorage = this.meta.useObjectStorage;
- this.objectStorageBaseUrl = this.meta.objectStorageBaseUrl;
- this.objectStorageBucket = this.meta.objectStorageBucket;
- this.objectStoragePrefix = this.meta.objectStoragePrefix;
- this.objectStorageEndpoint = this.meta.objectStorageEndpoint;
- this.objectStorageRegion = this.meta.objectStorageRegion;
- this.objectStoragePort = this.meta.objectStoragePort;
- this.objectStorageAccessKey = this.meta.objectStorageAccessKey;
- this.objectStorageSecretKey = this.meta.objectStorageSecretKey;
- this.objectStorageUseSSL = this.meta.objectStorageUseSSL;
- this.objectStorageUseProxy = this.meta.objectStorageUseProxy;
- this.objectStorageSetPublicRead = this.meta.objectStorageSetPublicRead;
- this.objectStorageS3ForcePathStyle = this.meta.objectStorageS3ForcePathStyle;
- this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
- this.twitterConsumerKey = this.meta.twitterConsumerKey;
- this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
- this.enableGithubIntegration = this.meta.enableGithubIntegration;
- this.githubClientId = this.meta.githubClientId;
- this.githubClientSecret = this.meta.githubClientSecret;
- this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
- this.discordClientId = this.meta.discordClientId;
- this.discordClientSecret = this.meta.discordClientSecret;
- this.useStarForReactionFallback = this.meta.useStarForReactionFallback;
- this.smtpSecure = this.meta.smtpSecure;
- this.smtpHost = this.meta.smtpHost;
- this.smtpPort = this.meta.smtpPort;
- this.smtpUser = this.meta.smtpUser;
- this.smtpPass = this.meta.smtpPass;
- this.summalyProxy = this.meta.summalyProxy;
-
- if (this.proxyAccountId) {
- os.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
- this.proxyAccount = proxyAccount;
- });
}
},
- mounted() {
- this.$watch('enableHcaptcha', () => {
- if (this.enableHcaptcha && this.enableRecaptcha) {
- os.dialog({
- type: 'question', // warning だと間違って cancel するかもしれない
- showCancelButton: true,
- title: this.$ts.settingGuide,
- text: this.$ts.avoidMultiCaptchaConfirm,
- }).then(({ canceled }) => {
- if (canceled) {
- return;
- }
-
- this.enableRecaptcha = false;
- });
- }
- });
-
- this.$watch('enableRecaptcha', () => {
- if (this.enableRecaptcha && this.enableHcaptcha) {
- os.dialog({
- type: 'question', // warning だと間違って cancel するかもしれない
- showCancelButton: true,
- title: this.$ts.settingGuide,
- text: this.$ts.avoidMultiCaptchaConfirm,
- }).then(({ canceled }) => {
- if (canceled) {
- return;
- }
-
- this.enableHcaptcha = false;
- });
- }
- });
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
},
methods: {
- invite() {
- os.api('admin/invite').then(x => {
- os.dialog({
- type: 'info',
- text: x.code
- });
- }).catch(e => {
- os.dialog({
- type: 'error',
- text: e
- });
- });
- },
-
- addPinUser() {
- os.selectUser().then(user => {
- this.pinnedUsers = this.pinnedUsers.trim();
- this.pinnedUsers += '\n@' + getAcct(user);
- this.pinnedUsers = this.pinnedUsers.trim();
- });
+ async init() {
+ const meta = await os.api('meta', { detail: true });
+ this.name = meta.name;
+ this.description = meta.description;
+ this.tosUrl = meta.tosUrl;
+ this.iconUrl = meta.iconUrl;
+ this.bannerUrl = meta.bannerUrl;
+ this.maintainerName = meta.maintainerName;
+ this.maintainerEmail = meta.maintainerEmail;
+ this.maxNoteTextLength = meta.maxNoteTextLength;
+ this.enableLocalTimeline = !meta.disableLocalTimeline;
+ this.enableGlobalTimeline = !meta.disableGlobalTimeline;
},
- chooseProxyAccount() {
- os.selectUser().then(user => {
- this.proxyAccount = user;
- this.proxyAccountId = user.id;
- this.save(true);
- });
- },
-
- async testEmail() {
- os.api('admin/send-email', {
- to: this.maintainerEmail,
- subject: 'Test email',
- text: 'Yo'
- }).then(x => {
- os.dialog({
- type: 'success',
- splash: true
- });
- }).catch(e => {
- os.dialog({
- type: 'error',
- text: e
- });
- });
- },
-
- save(withDialog = false) {
- os.api('admin/update-meta', {
+ save() {
+ os.apiWithDialog('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
- bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl,
- logoImageUrl: this.logoImageUrl,
- backgroundImageUrl: this.backgroundImageUrl,
+ bannerUrl: this.bannerUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
- disableRegistration: !this.enableRegistration,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
- enableHcaptcha: this.enableHcaptcha,
- hcaptchaSiteKey: this.hcaptchaSiteKey,
- hcaptchaSecretKey: this.hcaptchaSecretKey,
- enableRecaptcha: this.enableRecaptcha,
- recaptchaSiteKey: this.recaptchaSiteKey,
- recaptchaSecretKey: this.recaptchaSecretKey,
- proxyAccountId: this.proxyAccountId,
- cacheRemoteFiles: this.cacheRemoteFiles,
- proxyRemoteFiles: this.proxyRemoteFiles,
- localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
- remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
- blockedHosts: this.blockedHosts.split('\n') || [],
- pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
- pinnedPages: this.pinnedPages ? this.pinnedPages.split('\n') : [],
- pinnedClipId: (this.pinnedClipId && this.pinnedClipId) != '' ? this.pinnedClipId : null,
- enableServiceWorker: this.enableServiceWorker,
- swPublicKey: this.swPublicKey,
- swPrivateKey: this.swPrivateKey,
- useObjectStorage: this.useObjectStorage,
- objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
- objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
- objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
- objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
- objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
- objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
- objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
- objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
- objectStorageUseSSL: this.objectStorageUseSSL,
- objectStorageUseProxy: this.objectStorageUseProxy,
- objectStorageSetPublicRead: this.objectStorageSetPublicRead,
- objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
- enableTwitterIntegration: this.enableTwitterIntegration,
- twitterConsumerKey: this.twitterConsumerKey,
- twitterConsumerSecret: this.twitterConsumerSecret,
- enableGithubIntegration: this.enableGithubIntegration,
- githubClientId: this.githubClientId,
- githubClientSecret: this.githubClientSecret,
- enableDiscordIntegration: this.enableDiscordIntegration,
- discordClientId: this.discordClientId,
- discordClientSecret: this.discordClientSecret,
- enableEmail: this.enableEmail,
- email: this.email,
- smtpSecure: this.smtpSecure,
- smtpHost: this.smtpHost,
- smtpPort: this.smtpPort,
- smtpUser: this.smtpUser,
- smtpPass: this.smtpPass,
- summalyProxy: this.summalyProxy,
- useStarForReactionFallback: this.useStarForReactionFallback,
}).then(() => {
fetchInstance();
- if (withDialog) {
- os.success();
- }
- }).catch(e => {
- os.dialog({
- type: 'error',
- text: e
- });
});
}
}
diff --git a/src/client/pages/instance/user-dialog.vue b/src/client/pages/instance/user-dialog.vue
deleted file mode 100644
index d7d627191e..0000000000
--- a/src/client/pages/instance/user-dialog.vue
+++ /dev/null
@@ -1,230 +0,0 @@
-<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" :show-indicator="true"/>
- </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"><i class="fas fa-bookmark"></i></span>
- <span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
- <span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
- <span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
- </div>
- </div>
- <div class="_section">
- <div class="_content">
- <MkSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $ts.moderator }}</MkSwitch>
- <MkSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $ts.silence }}</MkSwitch>
- <MkSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $ts.suspend }}</MkSwitch>
- </div>
- </div>
- <div class="_section">
- <div class="_content">
- <MkButton full @click="openProfile"><i class="fas fa-external-link-square-alt"></i> {{ $ts.profile }}</MkButton>
- <MkButton full v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</MkButton>
- <MkButton full @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</MkButton>
- <MkButton full @click="deleteAllFiles" danger><i class="fas fa-trash-alt"></i> {{ $ts.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 MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import XModalWindow from '@client/components/ui/modal-window.vue';
-import Progress from '@client/scripts/loading';
-import { acct, userPage } from '../../filters/user';
-import * as os from '@client/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,
- };
- },
-
- 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.$ts.silenceConfirm : this.$ts.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.$ts.suspendConfirm : this.$ts.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.$ts.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/user.vue b/src/client/pages/instance/user.vue
new file mode 100644
index 0000000000..fbc10a3672
--- /dev/null
+++ b/src/client/pages/instance/user.vue
@@ -0,0 +1,229 @@
+<template>
+<FormBase>
+ <FormSuspense :p="init">
+ <div class="_formItem aeakzknw">
+ <MkAvatar class="avatar" :user="user" :show-indicator="true"/>
+ </div>
+
+ <FormLink :to="userPage(user)">Profile</FormLink>
+
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>Acct</template>
+ <template #value><span class="_monospace">{{ acct(user) }}</span></template>
+ </FormKeyValueView>
+
+ <FormKeyValueView>
+ <template #key>ID</template>
+ <template #value><span class="_monospace">{{ user.id }}</span></template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormGroup>
+ <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $ts.moderator }}</FormSwitch>
+ <FormSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $ts.silence }}</FormSwitch>
+ <FormSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $ts.suspend }}</FormSwitch>
+ </FormGroup>
+
+ <FormGroup>
+ <FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
+ <FormButton v-if="user.host == null" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
+ </FormGroup>
+
+ <FormGroup>
+ <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
+
+ <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
+ <FormKeyValueView v-else>
+ <template #key>{{ $ts.instanceInfo }}</template>
+ <template #value>(Local user)</template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>{{ $ts.updatedAt }}</template>
+ <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormObjectView tall :value="user">
+ <span>Raw</span>
+ </FormObjectView>
+ </FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { computed, defineAsyncComponent, defineComponent } from 'vue';
+import FormObjectView from '@client/components/form/object-view.vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormLink from '@client/components/form/link.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormButton from '@client/components/form/button.vue';
+import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import * as os from '@client/os';
+import number from '@client/filters/number';
+import bytes from '@client/filters/bytes';
+import * as symbols from '@client/symbols';
+import { url } from '@client/config';
+import { userPage, acct } from '@client/filters/user';
+
+export default defineComponent({
+ components: {
+ FormBase,
+ FormSwitch,
+ FormObjectView,
+ FormButton,
+ FormLink,
+ FormGroup,
+ FormKeyValueView,
+ FormSuspense,
+ },
+
+ props: {
+ userId: {
+ type: String,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: this.$ts.userInfo,
+ icon: 'fas fa-info-circle',
+ actions: this.user ? [this.user.url ? {
+ text: this.user.url,
+ icon: 'fas fa-external-link-alt',
+ handler: () => {
+ window.open(this.user.url, '_blank');
+ }
+ } : undefined].filter(x => x !== undefined) : [],
+ })),
+ init: null,
+ user: null,
+ info: null,
+ moderator: false,
+ silenced: false,
+ suspended: false,
+ }
+ },
+
+ watch: {
+ userId: {
+ handler() {
+ this.init = this.createFetcher();
+ },
+ immediate: true
+ }
+ },
+
+ methods: {
+ number,
+ bytes,
+ userPage,
+ acct,
+
+ createFetcher() {
+ return () => Promise.all([os.api('users/show', {
+ userId: this.userId
+ }), os.api('admin/show-user', {
+ userId: this.userId
+ })]).then(([user, info]) => {
+ this.user = user;
+ this.info = info;
+ this.moderator = this.info.isModerator;
+ this.silenced = this.info.isSilenced;
+ this.suspended = this.info.isSuspended;
+ });
+ },
+
+ refreshUser() {
+ this.init = this.createFetcher();
+ },
+
+ async updateRemoteUser() {
+ await os.apiWithDialog('admin/update-remote-user', { userId: this.user.id });
+ 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.$ts.silenceConfirm : this.$ts.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.$ts.suspendConfirm : this.$ts.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.$ts.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();
+ },
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.aeakzknw {
+ > .avatar {
+ display: block;
+ margin: 0 auto;
+ width: 64px;
+ height: 64px;
+ }
+}
+</style>
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue
index 4db965588c..452886abde 100644
--- a/src/client/pages/instance/users.vue
+++ b/src/client/pages/instance/users.vue
@@ -1,86 +1,71 @@
<template>
-<div class="mk-instance-users">
- <div class="_section">
- <div class="_content">
- <MkButton inline primary @click="addUser()"><i class="fas fa-plus"></i> {{ $ts.addUser }}</MkButton>
- </div>
+<div class="lknzcolw">
+ <div class="actions">
+ <MkButton inline primary @click="addUser()"><i class="fas fa-plus"></i> {{ $ts.addUser }}</MkButton>
+ <MkButton inline primary @click="lookupUser()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
</div>
- <div class="_section lookup">
- <div class="_title"><i class="fas fa-search"></i> {{ $ts.lookup }}</div>
- <div class="_content">
- <MkInput class="target" v-model:value="target" type="text" @enter="showUser()">
- <span>{{ $ts.usernameOrUserId }}</span>
+ <div class="users">
+ <div class="inputs" style="display: flex;">
+ <MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.sort }}</template>
+ <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
+ <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
+ </MkSelect>
+ <MkSelect v-model:value="state" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.state }}</template>
+ <option value="all">{{ $ts.all }}</option>
+ <option value="available">{{ $ts.normal }}</option>
+ <option value="admin">{{ $ts.administrator }}</option>
+ <option value="moderator">{{ $ts.moderator }}</option>
+ <option value="silenced">{{ $ts.silence }}</option>
+ <option value="suspended">{{ $ts.suspend }}</option>
+ </MkSelect>
+ <MkSelect v-model:value="origin" style="margin: 0; flex: 1;">
+ <template #label>{{ $ts.instance }}</template>
+ <option value="combined">{{ $ts.all }}</option>
+ <option value="local">{{ $ts.local }}</option>
+ <option value="remote">{{ $ts.remote }}</option>
+ </MkSelect>
+ </div>
+ <div class="inputs" style="display: flex; padding-top: 1.2em;">
+ <MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()">
+ <span>{{ $ts.username }}</span>
+ </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>{{ $ts.host }}</span>
</MkInput>
- <MkButton @click="showUser()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
</div>
- </div>
- <div class="_section users">
- <div class="_title"><i class="fas fa-users"></i> {{ $ts.users }}</div>
- <div class="_content">
- <div class="inputs" style="display: flex;">
- <MkSelect v-model:value="sort" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.sort }}</template>
- <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
- <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
- <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
- <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
- </MkSelect>
- <MkSelect v-model:value="state" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.state }}</template>
- <option value="all">{{ $ts.all }}</option>
- <option value="available">{{ $ts.normal }}</option>
- <option value="admin">{{ $ts.administrator }}</option>
- <option value="moderator">{{ $ts.moderator }}</option>
- <option value="silenced">{{ $ts.silence }}</option>
- <option value="suspended">{{ $ts.suspend }}</option>
- </MkSelect>
- <MkSelect v-model:value="origin" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.instance }}</template>
- <option value="combined">{{ $ts.all }}</option>
- <option value="local">{{ $ts.local }}</option>
- <option value="remote">{{ $ts.remote }}</option>
- </MkSelect>
- </div>
- <div class="inputs" style="display: flex; padding-top: 1.2em;">
- <MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.users.reload()">
- <span>{{ $ts.username }}</span>
- </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>{{ $ts.host }}</span>
- </MkInput>
- </div>
-
- <MkPagination :pagination="pagination" #default="{items}" class="users" ref="users">
- <button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)">
- <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
- <div class="body">
- <header>
- <MkUserName class="name" :user="user"/>
- <span class="acct">@{{ acct(user) }}</span>
- <span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span>
- <span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
- <span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
- <span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
- </header>
- <div>
- <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
- </div>
- <div>
- <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
- </div>
+ <MkPagination :pagination="pagination" #default="{items}" class="users" ref="users">
+ <button class="user _panel _button _gap" v-for="user in items" :key="user.id" @click="show(user)">
+ <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
+ <div class="body">
+ <header>
+ <MkUserName class="name" :user="user"/>
+ <span class="acct">@{{ acct(user) }}</span>
+ <span class="staff" v-if="user.isAdmin"><i class="fas fa-bookmark"></i></span>
+ <span class="staff" v-if="user.isModerator"><i class="far fa-bookmark"></i></span>
+ <span class="punished" v-if="user.isSilenced"><i class="fas fa-microphone-slash"></i></span>
+ <span class="punished" v-if="user.isSuspended"><i class="fas fa-snowflake"></i></span>
+ </header>
+ <div>
+ <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span>
</div>
- </button>
- </MkPagination>
- </div>
+ <div>
+ <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span>
+ </div>
+ </div>
+ </button>
+ </MkPagination>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import parseAcct from '@/misc/acct/parse';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/ui/input.vue';
import MkSelect from '@client/components/ui/select.vue';
@@ -88,6 +73,7 @@ import MkPagination from '@client/components/ui/pagination.vue';
import { acct } from '../../filters/user';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
+import { lookupUser } from '@client/scripts/lookup-user';
export default defineComponent({
components: {
@@ -97,6 +83,8 @@ export default defineComponent({
MkPagination,
},
+ emits: ['info'],
+
data() {
return {
[symbols.PAGE_INFO]: {
@@ -107,7 +95,6 @@ export default defineComponent({
handler: this.searchUser
}
},
- target: '',
sort: '+createdAt',
state: 'all',
origin: 'local',
@@ -140,40 +127,12 @@ export default defineComponent({
},
},
- methods: {
- /** テキストエリアのユーザーを解決する */
- fetchUser() {
- return new Promise((res) => {
- 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) {
- os.dialog({
- type: 'error',
- text: this.$ts.noSuchUser
- });
- } else {
- _notFound = true;
- }
- };
- usernamePromise.then(res).catch(e => {
- if (e.code === 'NO_SUCH_USER') {
- notFound();
- }
- });
- idPromise.then(res).catch(e => {
- notFound();
- });
- });
- },
+ async mounted() {
+ this.$emit('info', this[symbols.PAGE_INFO]);
+ },
- /** テキストエリアから処理対象ユーザーを設定する */
- async showUser() {
- const user = await this.fetchUser();
- this.show(user);
- this.target = '';
- },
+ methods: {
+ lookupUser,
searchUser() {
os.selectUser().then(user => {
@@ -203,9 +162,7 @@ export default defineComponent({
},
show(user) {
- os.popup(import('./user-dialog.vue'), {
- userId: user.id
- }, {}, 'closed');
+ os.pageWindow(`/instance/user/${user.id}`);
},
acct
@@ -214,57 +171,61 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-.mk-instance-users {
+.lknzcolw {
+ > .actions {
+ margin: var(--margin);
+ }
+
> .users {
- > ._content {
- > .users {
- margin-top: var(--margin);
+ margin: var(--margin);
+
+ > .users {
+ margin-top: var(--margin);
- > .user {
- display: flex;
- width: 100%;
- box-sizing: border-box;
- text-align: left;
- align-items: center;
- padding: 16px;
+ > .user {
+ display: flex;
+ width: 100%;
+ box-sizing: border-box;
+ text-align: left;
+ align-items: center;
+ padding: 16px;
- &:hover {
- color: var(--accent);
- }
+ &:hover {
+ color: var(--accent);
+ }
- > .avatar {
- width: 60px;
- height: 60px;
- }
+ > .avatar {
+ width: 60px;
+ height: 60px;
+ }
- > .body {
- margin-left: 0.3em;
- padding: 0 8px;
- flex: 1;
+ > .body {
+ margin-left: 0.3em;
+ padding: 0 8px;
+ flex: 1;
- @media (max-width: 500px) {
- font-size: 14px;
- }
+ @media (max-width: 500px) {
+ font-size: 14px;
+ }
- > header {
- > .name {
- font-weight: bold;
- }
+ > header {
+ > .name {
+ font-weight: bold;
+ }
- > .acct {
- margin-left: 8px;
- opacity: 0.7;
- }
+ > .acct {
+ margin-left: 8px;
+ opacity: 0.7;
+ }
- > .staff {
- margin-left: 0.5em;
- color: var(--badge);
- }
+ > .staff {
+ margin-left: 0.5em;
+ color: var(--badge);
+ }
- > .punished {
- margin-left: 0.5em;
- color: #4dabf7;
- }
+ > .punished {
+ margin-left: 0.5em;
+ color: #4dabf7;
}
}
}
diff --git a/src/client/pages/user-info.vue b/src/client/pages/user-info.vue
index ebe462f796..378fbb7b50 100644
--- a/src/client/pages/user-info.vue
+++ b/src/client/pages/user-info.vue
@@ -1,34 +1,36 @@
<template>
<FormBase>
- <FormGroup v-if="user">
- <template #label><MkAcct :user="user"/></template>
-
- <FormKeyValueView>
- <template #key>ID</template>
- <template #value><span class="_monospace">{{ user.id }}</span></template>
- </FormKeyValueView>
-
+ <FormSuspense :p="init">
<FormGroup>
- <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
-
- <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
- <FormKeyValueView v-else>
- <template #key>{{ $ts.instanceInfo }}</template>
- <template #value>(Local user)</template>
- </FormKeyValueView>
- </FormGroup>
+ <template #label><MkAcct :user="user"/></template>
- <FormGroup>
<FormKeyValueView>
- <template #key>{{ $ts.updatedAt }}</template>
- <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
+ <template #key>ID</template>
+ <template #value><span class="_monospace">{{ user.id }}</span></template>
</FormKeyValueView>
- </FormGroup>
- <FormObjectView tall :value="user">
- <span>Raw</span>
- </FormObjectView>
- </FormGroup>
+ <FormGroup>
+ <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
+
+ <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
+ <FormKeyValueView v-else>
+ <template #key>{{ $ts.instanceInfo }}</template>
+ <template #value>(Local user)</template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>{{ $ts.updatedAt }}</template>
+ <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormObjectView tall :value="user">
+ <span>Raw</span>
+ </FormObjectView>
+ </FormGroup>
+ </FormSuspense>
</FormBase>
</template>
@@ -80,23 +82,27 @@ export default defineComponent({
}
} : undefined].filter(x => x !== undefined) : [],
})),
+ init: null,
user: null,
}
},
- mounted() {
- this.fetch();
+ watch: {
+ userId: {
+ handler() {
+ this.init = () => os.api('users/show', {
+ userId: this.userId
+ }).then(user => {
+ this.user = user;
+ });
+ },
+ immediate: true
+ }
},
methods: {
number,
bytes,
-
- async fetch() {
- this.user = await os.api('users/show', {
- userId: this.userId
- });
- }
}
});
</script>
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 477f235062..207b44f631 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -195,7 +195,7 @@
<template v-if="page === 'index'">
<div>
- <div v-if="user.pinnedNotes.length > 0">
+ <div v-if="user.pinnedNotes.length > 0" class="_gap">
<XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
</div>
<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
diff --git a/src/client/router.ts b/src/client/router.ts
index bf45c806e2..93de287ea5 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -59,17 +59,9 @@ export const router = createRouter({
{ path: '/my/antennas', component: page('my-antennas/index') },
{ path: '/my/clips', component: page('my-clips/index') },
{ path: '/scratchpad', component: page('scratchpad') },
+ { path: '/instance/user/:user', component: page('instance/user'), props: route => ({ userId: route.params.user }) },
+ { path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
{ path: '/instance', component: page('instance/index') },
- { path: '/instance/emojis', component: page('instance/emojis') },
- { path: '/instance/users', component: page('instance/users') },
- { path: '/instance/logs', component: page('instance/logs') },
- { path: '/instance/files', component: page('instance/files') },
- { path: '/instance/queue', component: page('instance/queue') },
- { path: '/instance/settings', component: page('instance/settings') },
- { path: '/instance/federation', component: page('instance/federation') },
- { path: '/instance/relays', component: page('instance/relays') },
- { path: '/instance/announcements', component: page('instance/announcements') },
- { path: '/instance/abuses', component: page('instance/abuses') },
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts
index ceb2bfe173..9a003b5c38 100644
--- a/src/client/scripts/get-user-menu.ts
+++ b/src/client/scripts/get-user-menu.ts
@@ -124,7 +124,13 @@ export function getUserMenu(user) {
action: () => {
copyToClipboard(`@${user.username}@${user.host || host}`);
}
- }, {
+ }, ($i && ($i.isAdmin || $i.isModerator)) ? {
+ icon: 'fas fa-info-circle',
+ text: i18n.locale.info,
+ action: () => {
+ os.pageWindow(`/instance/user/${user.id}`);
+ }
+ } : {
icon: 'fas fa-info-circle',
text: i18n.locale.info,
action: () => {
diff --git a/src/client/scripts/lookup-user.ts b/src/client/scripts/lookup-user.ts
new file mode 100644
index 0000000000..1bcfd8e9db
--- /dev/null
+++ b/src/client/scripts/lookup-user.ts
@@ -0,0 +1,37 @@
+import parseAcct from '@/misc/acct/parse';
+import { i18n } from '@client/i18n';
+import * as os from '@client/os';
+
+export async function lookupUser() {
+ const { canceled, result } = await os.dialog({
+ title: i18n.locale.usernameOrUserId,
+ input: true
+ });
+ if (canceled) return;
+
+ const show = (user) => {
+ os.pageWindow(`/instance/user/${user.id}`);
+ };
+
+ const usernamePromise = os.api('users/show', parseAcct(result));
+ const idPromise = os.api('users/show', { userId: result });
+ let _notFound = false;
+ const notFound = () => {
+ if (_notFound) {
+ os.dialog({
+ type: 'error',
+ text: i18n.locale.noSuchUser
+ });
+ } else {
+ _notFound = true;
+ }
+ };
+ usernamePromise.then(show).catch(e => {
+ if (e.code === 'NO_SUCH_USER') {
+ notFound();
+ }
+ });
+ idPromise.then(show).catch(e => {
+ notFound();
+ });
+}
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
index fefe6afa7b..45b1c079bd 100644
--- a/src/client/ui/_common_/sidebar.vue
+++ b/src/client/ui/_common_/sidebar.vue
@@ -25,9 +25,9 @@
</component>
</template>
<div class="divider"></div>
- <button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$i.isAdmin || $i.isModerator" @click="oepnInstanceMenu">
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance">
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
- </button>
+ </MkA>
<button class="item _button" @click="more">
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
@@ -172,65 +172,6 @@ export default defineComponent({
});
},
- oepnInstanceMenu(ev) {
- os.modalMenu([{
- type: 'link',
- text: this.$ts.dashboard,
- to: '/instance',
- icon: 'fas fa-tachometer-alt',
- }, null, this.$i.isAdmin ? {
- type: 'link',
- text: this.$ts.settings,
- to: '/instance/settings',
- icon: 'fas fa-cog',
- } : undefined, {
- type: 'link',
- text: this.$ts.customEmojis,
- to: '/instance/emojis',
- icon: 'fas fa-laugh',
- }, {
- type: 'link',
- text: this.$ts.users,
- to: '/instance/users',
- icon: 'fas fa-users',
- }, {
- type: 'link',
- text: this.$ts.files,
- to: '/instance/files',
- icon: 'fas fa-cloud',
- }, {
- type: 'link',
- text: this.$ts.jobQueue,
- to: '/instance/queue',
- icon: 'fas fa-exchange-alt',
- }, {
- type: 'link',
- text: this.$ts.federation,
- to: '/instance/federation',
- icon: 'fas fa-globe',
- }, {
- type: 'link',
- text: this.$ts.relays,
- to: '/instance/relays',
- icon: 'fas fa-project-diagram',
- }, {
- type: 'link',
- text: this.$ts.announcements,
- to: '/instance/announcements',
- icon: 'fas fa-broadcast-tower',
- }, {
- type: 'link',
- text: this.$ts.abuseReports,
- to: '/instance/abuses',
- icon: 'fas fa-exclamation-circle',
- }, {
- type: 'link',
- text: this.$ts.logs,
- to: '/instance/logs',
- icon: 'fas fa-stream',
- }], ev.currentTarget || ev.target);
- },
-
more(ev) {
os.popup(import('@client/components/launch-pad.vue'), {}, {
}, 'closed');
diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue
index 952ec3903d..29ef99fc86 100644
--- a/src/client/ui/default.sidebar.vue
+++ b/src/client/ui/default.sidebar.vue
@@ -20,9 +20,9 @@
</component>
</template>
<div class="divider"></div>
- <button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$i.isAdmin || $i.isModerator" @click="oepnInstanceMenu">
+ <MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null">
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
- </button>
+ </MkA>
<button class="item _button" @click="more">
<i class="fas fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
@@ -156,65 +156,6 @@ export default defineComponent({
});
},
- oepnInstanceMenu(ev) {
- os.modalMenu([{
- type: 'link',
- text: this.$ts.dashboard,
- to: '/instance',
- icon: 'fas fa-tachometer-alt',
- }, null, this.$i.isAdmin ? {
- type: 'link',
- text: this.$ts.settings,
- to: '/instance/settings',
- icon: 'fas fa-cog',
- } : undefined, {
- type: 'link',
- text: this.$ts.customEmojis,
- to: '/instance/emojis',
- icon: 'fas fa-laugh',
- }, {
- type: 'link',
- text: this.$ts.users,
- to: '/instance/users',
- icon: 'fas fa-users',
- }, {
- type: 'link',
- text: this.$ts.files,
- to: '/instance/files',
- icon: 'fas fa-cloud',
- }, {
- type: 'link',
- text: this.$ts.jobQueue,
- to: '/instance/queue',
- icon: 'fas fa-exchange-alt',
- }, {
- type: 'link',
- text: this.$ts.federation,
- to: '/instance/federation',
- icon: 'fas fa-globe',
- }, {
- type: 'link',
- text: this.$ts.relays,
- to: '/instance/relays',
- icon: 'fas fa-project-diagram',
- }, {
- type: 'link',
- text: this.$ts.announcements,
- to: '/instance/announcements',
- icon: 'fas fa-broadcast-tower',
- }, {
- type: 'link',
- text: this.$ts.abuseReports,
- to: '/instance/abuses',
- icon: 'fas fa-exclamation-circle',
- }, {
- type: 'link',
- text: this.$ts.logs,
- to: '/instance/logs',
- icon: 'fas fa-stream',
- }], ev.currentTarget || ev.target);
- },
-
more(ev) {
os.popup(import('@client/components/launch-pad.vue'), {}, {
}, 'closed');