diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index c4d0eb85dd..1959271f5d 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -170,10 +170,10 @@ export default defineComponent({ aspectRatio: props.aspectRatio || 2.5, layout: { padding: { - left: 16, - right: 16, - top: 16, - bottom: 8, + left: 0, + right: 0, + top: 0, + bottom: 0, }, }, scales: { diff --git a/packages/client/src/components/form/section.vue b/packages/client/src/components/form/section.vue index ab9fbe5fcd..c6e34ef1cc 100644 --- a/packages/client/src/components/form/section.vue +++ b/packages/client/src/components/form/section.vue @@ -1,5 +1,5 @@ <template> -<div v-size="{ max: [500] }" v-sticky-container class="vrtktovh _formBlock"> +<div class="vrtktovh _formBlock"> <div class="label"><slot name="label"></slot></div> <div class="main _formRoot"> <slot></slot> @@ -12,10 +12,8 @@ <style lang="scss" scoped> .vrtktovh { - margin: 0; border-top: solid 0.5px var(--divider); border-bottom: solid 0.5px var(--divider); - padding: 24px 0; & + .vrtktovh { border-top: none; @@ -31,7 +29,7 @@ > .label { font-weight: bold; - padding: 0 0 16px 0; + margin: 1.5em 0 16px 0; &:empty { display: none; @@ -39,6 +37,7 @@ } > .main { + margin: 1.5em 0; } } </style> diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue index ac3284e7da..f8a07b4caa 100644 --- a/packages/client/src/components/form/switch.vue +++ b/packages/client/src/components/form/switch.vue @@ -111,7 +111,7 @@ export default defineComponent({ } > .label { - margin-left: 16px; + margin-left: 12px; margin-top: 2px; display: block; transition: inherit; diff --git a/packages/client/src/components/global/spacer.vue b/packages/client/src/components/global/spacer.vue index e2f1d1aec7..8a1d7a4e8a 100644 --- a/packages/client/src/components/global/spacer.vue +++ b/packages/client/src/components/global/spacer.vue @@ -40,7 +40,7 @@ export default defineComponent({ return; } - if (rect.width > props.contentMax || rect.width > 500) { + if (rect.width > props.contentMax || (rect.width > 360 && window.innerWidth > 400)) { margin.value = props.marginMax; } else { margin.value = props.marginMin; diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue index bc62998a4a..409c3a49ca 100644 --- a/packages/client/src/components/instance-stats.vue +++ b/packages/client/src/components/instance-stats.vue @@ -1,5 +1,5 @@ <template> -<div class="zbcjwnqg" style="margin-top: -8px;"> +<div class="zbcjwnqg"> <div class="selects" style="display: flex;"> <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> <optgroup :label="$ts.federation"> @@ -29,16 +29,16 @@ <option value="day">{{ $ts.perDay }}</option> </MkSelect> </div> - <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart> + <div class="chart"> + <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart> + </div> </div> </template> <script lang="ts"> -import { defineComponent, onMounted, ref, watch } from 'vue'; +import { defineComponent, ref } from 'vue'; import MkSelect from '@/components/form/select.vue'; import MkChart from '@/components/chart.vue'; -import * as os from '@/os'; -import { defaultStore } from '@/store'; export default defineComponent({ components: { @@ -74,7 +74,10 @@ export default defineComponent({ <style lang="scss" scoped> .zbcjwnqg { > .selects { - padding: 8px 16px 0 16px; + } + + > .chart { + padding: 8px 0 0 0; } } </style> diff --git a/packages/client/src/components/key-value.vue b/packages/client/src/components/key-value.vue index 6a9a948ce9..da98abd77c 100644 --- a/packages/client/src/components/key-value.vue +++ b/packages/client/src/components/key-value.vue @@ -1,5 +1,5 @@ <template> -<div class="alqyeyti"> +<div class="alqyeyti" :class="{ oneline }"> <div class="key"> <slot name="key"></slot> </div> @@ -22,6 +22,11 @@ export default defineComponent({ required: false, default: null, }, + oneline: { + type: Boolean, + required: false, + default: false, + }, }, setup(props) { @@ -39,10 +44,30 @@ export default defineComponent({ <style lang="scss" scoped> .alqyeyti { + > .key, > .value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + > .key { font-size: 0.85em; padding: 0 0 0.25em 0; opacity: 0.75; } + + &.oneline { + display: flex; + + > .key { + width: 30%; + font-size: 1em; + padding: 0 8px 0 0; + } + + > .value { + width: 70%; + } + } } </style> diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index 618e569839..e9d17feec1 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -93,7 +93,8 @@ export default defineComponent({ return { [symbols.PAGE_INFO]: { title: this.$ts.instanceInfo, - icon: 'fas fa-info-circle' + icon: 'fas fa-info-circle', + bg: 'var(--bg)', }, host, version, diff --git a/packages/client/src/pages/admin/instance.vue b/packages/client/src/pages/admin/instance.vue deleted file mode 100644 index 51fcb8675a..0000000000 --- a/packages/client/src/pages/admin/instance.vue +++ /dev/null @@ -1,291 +0,0 @@ -<template> -<XModalWindow ref="dialog" - :width="520" - :height="500" - @close="$refs.dialog.close()" - @closed="$emit('closed')" -> - <template #header>{{ instance.host }}</template> - <div class="mk-instance-info"> - <div class="_table section"> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.software }}</div> - <div class="_data">{{ instance.softwareName || '?' }}</div> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.version }}</div> - <div class="_data">{{ instance.softwareVersion || '?' }}</div> - </div> - </div> - </div> - <div class="_table data section"> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.registeredAt }}</div> - <div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.following }}</div> - <button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.followers }}</div> - <button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.users }}</div> - <button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.notes }}</div> - <div class="_data">{{ number(instance.notesCount) }}</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.files }}</div> - <div class="_data">{{ number(instance.driveFiles) }}</div> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.storageUsage }}</div> - <div class="_data">{{ bytes(instance.driveUsage) }}</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.latestRequestSentAt }}</div> - <div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> - </div> - <div class="_cell"> - <div class="_label">{{ $ts.latestStatus }}</div> - <div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div> - </div> - </div> - <div class="_row"> - <div class="_cell"> - <div class="_label">{{ $ts.latestRequestReceivedAt }}</div> - <div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> - </div> - </div> - </div> - <div class="chart"> - <div class="header"> - <span class="label">{{ $ts.charts }}</span> - <div class="selects"> - <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> - <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> - <option value="instance-users">{{ $ts._instanceCharts.users }}</option> - <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> - <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option> - <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option> - <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option> - <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option> - <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> - <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> - <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option> - <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> - </MkSelect> - <MkSelect v-model="chartSpan" style="margin: 0;"> - <option value="hour">{{ $ts.perHour }}</option> - <option value="day">{{ $ts.perDay }}</option> - </MkSelect> - </div> - </div> - <div class="chart"> - <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart> - </div> - </div> - <div class="operations section"> - <span class="label">{{ $ts.operations }}</span> - <MkSwitch v-model="isSuspended" class="switch">{{ $ts.stopActivityDelivery }}</MkSwitch> - <MkSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</MkSwitch> - <details> - <summary>{{ $ts.deleteAllFiles }}</summary> - <MkButton style="margin: 0.5em 0 0.5em 0;" @click="deleteAllFiles()"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton> - </details> - <details> - <summary>{{ $ts.removeAllFollowing }}</summary> - <MkButton style="margin: 0.5em 0 0.5em 0;" @click="removeAllFollowing()"><i class="fas fa-minus-circle"></i> {{ $ts.removeAllFollowing }}</MkButton> - <MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo> - </details> - </div> - <details class="metadata section"> - <summary class="label">{{ $ts.metadata }}</summary> - <pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre> - </details> - </div> -</XModalWindow> -</template> - -<script lang="ts"> -import { defineComponent, markRaw } from 'vue'; -import XModalWindow from '@/components/ui/modal-window.vue'; -import MkSelect from '@/components/form/select.vue'; -import MkButton from '@/components/ui/button.vue'; -import MkSwitch from '@/components/form/switch.vue'; -import MkInfo from '@/components/ui/info.vue'; -import MkChart from '@/components/chart.vue'; -import bytes from '@/filters/bytes'; -import number from '@/filters/number'; -import * as os from '@/os'; - -export default defineComponent({ - components: { - XModalWindow, - MkSelect, - MkButton, - MkSwitch, - MkInfo, - MkChart, - }, - - props: { - instance: { - type: Object, - required: true - } - }, - - emits: ['closed'], - - data() { - return { - isSuspended: this.instance.isSuspended, - chartSrc: 'requests', - chartSpan: 'hour', - }; - }, - - computed: { - meta() { - return this.$instance; - }, - - isBlocked() { - return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host); - } - }, - - watch: { - isSuspended() { - os.api('admin/federation/update-instance', { - host: this.instance.host, - isSuspended: this.isSuspended - }); - }, - }, - - methods: { - changeBlock(e) { - os.api('admin/update-meta', { - blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host) - }); - }, - - removeAllFollowing() { - os.apiWithDialog('admin/federation/remove-all-following', { - host: this.instance.host - }); - }, - - deleteAllFiles() { - os.apiWithDialog('admin/federation/delete-all-files', { - host: this.instance.host - }); - }, - - showFollowing() { - // TODO: ページ遷移 - }, - - showFollowers() { - // TODO: ページ遷移 - }, - - showUsers() { - // TODO: ページ遷移 - }, - - bytes, - - number - } -}); -</script> - -<style lang="scss" scoped> -.mk-instance-info { - overflow: auto; - - > .section { - padding: 16px 32px; - - @media (max-width: 500px) { - padding: 8px 16px; - } - - &:not(:first-child) { - border-top: solid 0.5px var(--divider); - } - } - - > .chart { - border-top: solid 0.5px var(--divider); - padding: 16px 0 12px 0; - - > .header { - padding: 0 32px; - - @media (max-width: 500px) { - padding: 0 16px; - } - - > .label { - font-size: 80%; - opacity: 0.7; - } - - > .selects { - display: flex; - } - } - - > .chart { - padding: 0 16px; - - @media (max-width: 500px) { - padding: 0; - } - } - } - - > .operations { - > .label { - font-size: 80%; - opacity: 0.7; - } - - > .switch { - margin: 16px 0; - } - } - - > .metadata { - > .label { - font-size: 80%; - opacity: 0.7; - } - - > pre > code { - display: block; - max-height: 200px; - overflow: auto; - } - } -} -</style> diff --git a/packages/client/src/pages/admin/metrics.vue b/packages/client/src/pages/admin/metrics.vue index f566061ceb..1de297fd93 100644 --- a/packages/client/src/pages/admin/metrics.vue +++ b/packages/client/src/pages/admin/metrics.vue @@ -76,7 +76,6 @@ import MkwFederation from '../../widgets/federation.vue'; import { version, url } from '@/config'; import bytes from '@/filters/bytes'; import number from '@/filters/number'; -import MkInstanceInfo from './instance.vue'; Chart.register( ArcElement, diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue index 3dcfce01e5..564a63fda0 100644 --- a/packages/client/src/pages/admin/overview.vue +++ b/packages/client/src/pages/admin/overview.vue @@ -19,7 +19,7 @@ <MkContainer :foldable="true" class="charts"> <template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template> - <div style="padding-top: 12px;"> + <div style="padding: 12px;"> <MkInstanceStats :chart-limit="500" :detailed="true"/> </div> </MkContainer> @@ -77,7 +77,6 @@ import MkQueueChart from '@/components/queue-chart.vue'; import { version, url } from '@/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 '@/os'; import { stream } from '@/stream'; @@ -159,9 +158,7 @@ export default defineComponent({ host: q }); } - os.popup(MkInstanceInfo, { - instance: instance - }, {}, 'closed'); + // TODO }, bytes, diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index 85096d991a..d6d72f6601 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -1,70 +1,71 @@ <template> -<FormBase> - <FormGroup v-if="instance"> - <template #label>{{ instance.host }}</template> - <FormGroup> - <div class="_debobigegoItem"> - <div class="_debobigegoPanel fnfelxur"> - <img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> - </div> - </div> - <FormKeyValueView> - <template #key>Name</template> - <template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - </FormGroup> +<MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> + <div v-if="instance" class="_formRoot"> + <div class="fnfelxur"> + <img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> + </div> + <MkKeyValue :copy="host" oneline style="margin: 1em 0;"> + <template #key>Host</template> + <template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>Name</template> + <template #value>{{ instance.name || `(${$ts.unknown})` }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ $ts.description }}</template> + <template #value>{{ instance.description }}</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.software }}</template> + <template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }} / {{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.administrator }}</template> + <template #value>{{ instance.maintainerName || `(${$ts.unknown})` }} ({{ instance.maintainerEmail || `(${$ts.unknown})` }})</template> + </MkKeyValue> - <FormButton v-if="$i.isAdmin || $i.isModerator" primary @click="info">{{ $ts.settings }}</FormButton> + <FormSection v-if="iAmModerator"> + <template #label>Moderation</template> + <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch> + <FormSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</FormSwitch> + </FormSection> - <FormTextarea readonly :value="instance.description"> - <span>{{ $ts.description }}</span> - </FormTextarea> - - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.software }}</template> - <template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.version }}</template> - <template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.administrator }}</template> - <template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.contact }}</template> - <template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <FormKeyValueView> + <FormSection> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.registeredAt }}</template> + <template #value><MkTime mode="detail" :time="instance.caughtAt"/></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.updatedAt }}</template> + <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.latestRequestSentAt }}</template> <template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.latestStatus }}</template> <template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template> - </FormKeyValueView> - <FormKeyValueView> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>{{ $ts.latestRequestReceivedAt }}</template> <template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> - </FormKeyValueView> - </FormGroup> - <FormGroup> - <FormKeyValueView> + </MkKeyValue> + </FormSection> + + <FormSection> + <MkKeyValue oneline style="margin: 1em 0;"> <template #key>Open Registrations</template> <template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - </FormGroup> - <div class="_debobigegoItem"> - <div class="_debobigegoLabel">{{ $ts.statistics }}</div> - <div class="_debobigegoPanel cmhjzshl"> + </MkKeyValue> + </FormSection> + + <FormSection> + <template #label>{{ $ts.statistics }}</template> + <div class="cmhjzshl"> <div class="selects"> - <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> + <MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;"> <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> <option value="instance-users">{{ $ts._instanceCharts.users }}</option> <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> @@ -83,86 +84,56 @@ </MkSelect> </div> <div class="chart"> - <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart> + <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :args="{ host: host }" :detailed="true"></MkChart> </div> </div> - </div> - <FormGroup> - <FormKeyValueView> - <template #key>{{ $ts.registeredAt }}</template> - <template #value><MkTime mode="detail" :time="instance.caughtAt"/></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>{{ $ts.updatedAt }}</template> - <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template> - </FormKeyValueView> - </FormGroup> + </FormSection> + <FormObjectView tall :value="instance"> <span>Raw</span> </FormObjectView> - <FormGroup> + + <FormSection> <template #label>Well-known resources</template> - <FormLink :to="`https://${host}/.well-known/host-meta`" external>host-meta</FormLink> - <FormLink :to="`https://${host}/.well-known/host-meta.json`" external>host-meta.json</FormLink> - <FormLink :to="`https://${host}/.well-known/nodeinfo`" external>nodeinfo</FormLink> - <FormLink :to="`https://${host}/robots.txt`" external>robots.txt</FormLink> - <FormLink :to="`https://${host}/manifest.json`" external>manifest.json</FormLink> - </FormGroup> - <FormSuspense v-slot="{ result: dns }" :p="dnsPromiseFactory"> - <FormGroup> - <template #label>DNS</template> - <FormKeyValueView v-for="record in dns.a" :key="record"> - <template #key>A</template> - <template #value><span class="_monospace">{{ record }}</span></template> - </FormKeyValueView> - <FormKeyValueView v-for="record in dns.aaaa" :key="record"> - <template #key>AAAA</template> - <template #value><span class="_monospace">{{ record }}</span></template> - </FormKeyValueView> - <FormKeyValueView v-for="record in dns.cname" :key="record"> - <template #key>CNAME</template> - <template #value><span class="_monospace">{{ record }}</span></template> - </FormKeyValueView> - <FormKeyValueView v-for="record in dns.txt"> - <template #key>TXT</template> - <template #value><span class="_monospace">{{ record[0] }}</span></template> - </FormKeyValueView> - </FormGroup> - </FormSuspense> - </FormGroup> -</FormBase> + <FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink> + <FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink> + <FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink> + <FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink> + <FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink> + </FormSection> + </div> +</MkSpacer> </template> <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; import MkChart from '@/components/chart.vue'; import FormObjectView from '@/components/debobigego/object-view.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormTextarea from '@/components/form/textarea.vue'; +import FormLink from '@/components/form/link.vue'; +import MkLink from '@/components/link.vue'; +import FormSection from '@/components/form/section.vue'; +import FormButton from '@/components/ui/button.vue'; +import MkKeyValue from '@/components/key-value.vue'; import MkSelect from '@/components/form/select.vue'; +import FormSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; import number from '@/filters/number'; import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; -import MkInstanceInfo from '@/pages/admin/instance.vue'; export default defineComponent({ components: { - FormBase, FormTextarea, FormObjectView, FormButton, FormLink, - FormGroup, - FormKeyValueView, - FormSuspense, + FormSection, + FormSwitch, + MkKeyValue, MkSelect, MkChart, + MkLink, }, props: { @@ -175,8 +146,9 @@ export default defineComponent({ data() { return { [symbols.PAGE_INFO]: { - title: this.$ts.instanceInfo, + title: this.host, icon: 'fas fa-info-circle', + bg: 'var(--bg)', actions: [{ text: `https://${this.host}`, icon: 'fas fa-external-link-alt', @@ -186,14 +158,22 @@ export default defineComponent({ }], }, instance: null, - dnsPromiseFactory: () => os.api('federation/dns', { - host: this.host - }), + suspended: false, chartSrc: 'instance-requests', chartSpan: 'hour', } }, + computed: { + iAmModerator(): boolean { + return this.$i && (this.$i.isAdmin || this.$i.isModerator); + }, + + isBlocked() { + return this.instance && this.$instance && this.$instance.blockedHosts && this.$instance.blockedHosts.includes(this.instance.host); + } + }, + mounted() { this.fetch(); }, @@ -206,24 +186,30 @@ export default defineComponent({ this.instance = await os.api('federation/show-instance', { host: this.host }); + this.suspended = this.instance.isSuspended; }, - info() { - os.popup(MkInstanceInfo, { - instance: this.instance - }, {}, 'closed'); - } + changeBlock(e) { + os.api('admin/update-meta', { + blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host) + }); + }, + + async toggleSuspend(v) { + await os.api('admin/federation/update-instance', { + host: this.instance.host, + isSuspended: this.suspended + }); + }, } }); </script> <style lang="scss" scoped> .fnfelxur { - padding: 16px; - > .icon { display: block; - margin: auto; + margin: 0; height: 64px; border-radius: 8px; } @@ -232,7 +218,7 @@ export default defineComponent({ .cmhjzshl { > .selects { display: flex; - padding: 16px; + margin: 0 0 16px 0; } } </style> diff --git a/packages/client/src/pages/user-ap-info.vue b/packages/client/src/pages/user-ap-info.vue deleted file mode 100644 index 0027381f53..0000000000 --- a/packages/client/src/pages/user-ap-info.vue +++ /dev/null @@ -1,124 +0,0 @@ -<template> -<FormBase> - <FormSuspense v-slot="{ result: ap }" :p="apPromiseFactory"> - <FormGroup> - <template #label>ActivityPub</template> - <FormKeyValueView> - <template #key>Type</template> - <template #value><span class="_monospace">{{ ap.type }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>URI</template> - <template #value><span class="_monospace">{{ ap.id }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>URL</template> - <template #value><span class="_monospace">{{ ap.url }}</span></template> - </FormKeyValueView> - <FormGroup> - <FormKeyValueView> - <template #key>Inbox</template> - <template #value><span class="_monospace">{{ ap.inbox }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>Shared Inbox</template> - <template #value><span class="_monospace">{{ ap.sharedInbox || ap.endpoints.sharedInbox }}</span></template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>Outbox</template> - <template #value><span class="_monospace">{{ ap.outbox }}</span></template> - </FormKeyValueView> - </FormGroup> - <FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem"> - <span>Public Key</span> - </FormTextarea> - <FormKeyValueView> - <template #key>Discoverable</template> - <template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormKeyValueView> - <template #key>ManuallyApprovesFollowers</template> - <template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template> - </FormKeyValueView> - <FormObjectView tall :value="ap"> - <span>Raw</span> - </FormObjectView> - <FormGroup> - <FormLink :to="`https://${user.host}/.well-known/webfinger?resource=acct:${user.username}`" external>WebFinger</FormLink> - </FormGroup> - <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> - </FormSuspense> -</FormBase> -</template> - -<script lang="ts"> -import { defineAsyncComponent, defineComponent } from 'vue'; -import FormObjectView from '@/components/debobigego/object-view.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; -import * as os from '@/os'; -import number from '@/filters/number'; -import bytes from '@/filters/bytes'; -import * as symbols from '@/symbols'; -import { url } from '@/config'; - -export default defineComponent({ - components: { - FormBase, - FormTextarea, - FormObjectView, - FormButton, - FormLink, - FormGroup, - FormKeyValueView, - FormSuspense, - }, - - props: { - userId: { - type: String, - required: true - } - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.userInfo, - icon: 'fas fa-info-circle' - }, - user: null, - apPromiseFactory: null, - } - }, - - mounted() { - this.fetch(); - }, - - methods: { - number, - bytes, - - async fetch() { - this.user = await os.api('users/show', { - userId: this.userId - }); - - this.apPromiseFactory = () => os.api('ap/get', { - uri: this.user.uri || `${url}/users/${this.user.id}` - }); - } - } -}); -</script> diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 0fd208a64a..e3c10c6df9 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -1,70 +1,76 @@ <template> -<FormBase> +<MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> - <div class="_debobigegoItem aeakzknw"> - <MkAvatar class="avatar" :user="user" :show-indicator="true"/> + <div class="_formRoot"> + <div class="_formBlock aeakzknw"> + <MkAvatar class="avatar" :user="user" :show-indicator="true"/> + </div> + + <FormLink :to="userPage(user)">Profile</FormLink> + + <div class="_formBlock"> + <MkKeyValue :copy="acct(user)" oneline style="margin: 1em 0;"> + <template #key>Acct</template> + <template #value><span class="_monospace">{{ acct(user) }}</span></template> + </MkKeyValue> + + <MkKeyValue :copy="user.id" oneline style="margin: 1em 0;"> + <template #key>ID</template> + <template #value><span class="_monospace">{{ user.id }}</span></template> + </MkKeyValue> + </div> + + <FormSection v-if="iAmModerator"> + <template #label>Moderation</template> + <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> + <FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> + <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch> + <FormButton v-if="user.host == null && iAmModerator" class="_formBlock" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton> + </FormSection> + + <FormSection> + <template #label>ActivityPub</template> + + <div class="_formBlock"> + <MkKeyValue v-if="user.host" oneline style="margin: 1em 0;"> + <template #key>{{ $ts.instanceInfo }}</template> + <template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="fas fa-angle-right"></i></MkA></template> + </MkKeyValue> + <MkKeyValue v-else oneline style="margin: 1em 0;"> + <template #key>{{ $ts.instanceInfo }}</template> + <template #value>(Local user)</template> + </MkKeyValue> + <MkKeyValue oneline style="margin: 1em 0;"> + <template #key>{{ $ts.updatedAt }}</template> + <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template> + </MkKeyValue> + <MkKeyValue v-if="ap" oneline style="margin: 1em 0;"> + <template #key>Type</template> + <template #value><span class="_monospace">{{ ap.type }}</span></template> + </MkKeyValue> + </div> + + <FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton> + </FormSection> + + <FormObjectView tall :value="user"> + <span>Raw</span> + </FormObjectView> </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 v-if="iAmModerator"> - <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> - <FormSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> - <FormSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ $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 && iAmModerator" @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> +</MkSpacer> </template> <script lang="ts"> import { computed, defineAsyncComponent, defineComponent } from 'vue'; import FormObjectView from '@/components/debobigego/object-view.vue'; -import FormTextarea from '@/components/debobigego/textarea.vue'; -import FormSwitch from '@/components/debobigego/switch.vue'; -import FormLink from '@/components/debobigego/link.vue'; -import FormBase from '@/components/debobigego/base.vue'; -import FormGroup from '@/components/debobigego/group.vue'; -import FormButton from '@/components/debobigego/button.vue'; -import FormKeyValueView from '@/components/debobigego/key-value-view.vue'; -import FormSuspense from '@/components/debobigego/suspense.vue'; +import FormTextarea from '@/components/form/textarea.vue'; +import FormSwitch from '@/components/form/switch.vue'; +import FormLink from '@/components/form/link.vue'; +import FormSection from '@/components/form/section.vue'; +import FormButton from '@/components/ui/button.vue'; +import MkKeyValue from '@/components/key-value.vue'; +import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; import number from '@/filters/number'; import bytes from '@/filters/bytes'; @@ -74,14 +80,13 @@ import { userPage, acct } from '@/filters/user'; export default defineComponent({ components: { - FormBase, + FormSection, FormTextarea, FormSwitch, FormObjectView, FormButton, FormLink, - FormGroup, - FormKeyValueView, + MkKeyValue, FormSuspense, }, @@ -97,6 +102,7 @@ export default defineComponent({ [symbols.PAGE_INFO]: computed(() => ({ title: this.user ? acct(this.user) : this.$ts.userInfo, icon: 'fas fa-info-circle', + bg: 'var(--bg)', actions: this.user ? [this.user.url ? { text: this.user.url, icon: 'fas fa-external-link-alt', @@ -108,6 +114,7 @@ export default defineComponent({ init: null, user: null, info: null, + ap: null, moderator: false, silenced: false, suspended: false, @@ -126,6 +133,13 @@ export default defineComponent({ this.init = this.createFetcher(); }, immediate: true + }, + user() { + os.api('ap/get', { + uri: this.user.uri || `${url}/users/${this.user.id}` + }).then(res => { + this.ap = res; + }); } }, @@ -234,7 +248,6 @@ export default defineComponent({ .aeakzknw { > .avatar { display: block; - margin: 0 auto; width: 64px; height: 64px; } diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index 9b4dd162f3..2a07ff37cd 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -73,7 +73,6 @@ const defaultRoutes = [ { 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 }) }, - { path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) }, { path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) }, { path: '/games/reversi', component: page('reversi/index') }, { path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },