NOCHANGELOG Refactor of moderation detail pages

This commit is contained in:
Arne Bollinger 2025-05-07 14:55:15 +00:00 committed by petitminion
parent cd18b88799
commit 36030c68ee
16 changed files with 1238 additions and 1194 deletions

View File

@ -5,6 +5,8 @@ import { ref } from 'vue'
import axios from 'axios' import axios from 'axios'
import Button from '~/components/ui/Button.vue'
interface Events { interface Events {
(e: 'action-done', data: any): void (e: 'action-done', data: any): void
(e: 'action-error', error: BackendError): void (e: 'action-error', error: BackendError): void
@ -34,10 +36,13 @@ const ajaxCall = async () => {
</script> </script>
<template> <template>
<button <Button
:class="['ui', {loading: isLoading}, 'button']" secondary
low-height
:class="{loading: isLoading}"
icon="bi-arrow-clockwise"
@click="ajaxCall" @click="ajaxCall"
> >
<slot /> <slot />
</button> </Button>
</template> </template>

View File

@ -2,6 +2,8 @@
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Button from '~/components/ui/Button.vue'
interface Events { interface Events {
(e: 'update:modelValue', value: boolean): void (e: 'update:modelValue', value: boolean): void
} }
@ -18,8 +20,11 @@ const value = useVModel(props, 'modelValue', emit)
</script> </script>
<template> <template>
<a <Button
role="button" secondary
low-height
tiny
:icon="value ? 'bi-chevron-expand' : 'bi-chevron-contract'"
class="collapse link" class="collapse link"
@click.prevent="value = !value" @click.prevent="value = !value"
> >
@ -30,5 +35,5 @@ const value = useVModel(props, 'modelValue', emit)
{{ t('components.common.CollapseLink.button.collapse') }} {{ t('components.common.CollapseLink.button.collapse') }}
</span> </span>
<i :class="[{ down: !value, right: value }, 'angle', 'icon']" /> <i :class="[{ down: !value, right: value }, 'angle', 'icon']" />
</a> </Button>
</template> </template>

View File

@ -4,6 +4,8 @@ import { useI18n } from 'vue-i18n'
import useMarkdown from '~/composables/useMarkdown' import useMarkdown from '~/composables/useMarkdown'
import Button from '~/components/ui/Button.vue'
interface Events { interface Events {
(e: 'update'): void (e: 'update'): void
} }
@ -24,21 +26,21 @@ const summary = useMarkdown(() => props.object.summary)
<div> <div>
<slot /> <slot />
<p> <p>
<i class="clock outline icon" /><human-date :date="object.creation_date" /> &nbsp; <i class="bi bi-clock" /><human-date :date="object.creation_date" /> &nbsp;
<i class="user icon" />{{ object.actor }} &nbsp; <i class="bi bi-person" />{{ object.actor }} &nbsp;
<template v-if="object.is_active"> <template v-if="object.is_active">
<i class="play icon" /> <i class="bi bi-play" />
{{ t('components.manage.moderation.InstancePolicyCard.status.enabled') }} {{ t('components.manage.moderation.InstancePolicyCard.status.enabled') }}
</template> </template>
<template v-if="!object.is_active"> <template v-if="!object.is_active">
<i class="pause icon" /> <i class="bi bi-pause" />
{{ t('components.manage.moderation.InstancePolicyCard.status.paused') }} {{ t('components.manage.moderation.InstancePolicyCard.status.paused') }}
</template> </template>
</p> </p>
<div> <div>
<p><strong>{{ t('components.manage.moderation.InstancePolicyCard.header.rule') }}</strong></p> <p><strong>{{ t('components.manage.moderation.InstancePolicyCard.header.rule') }}</strong></p>
<p v-if="object.block_all"> <p v-if="object.block_all">
<i class="ban icon" /> <i class="bi bi-ban" />
{{ t('components.manage.moderation.InstancePolicyCard.label.blockAll') }} {{ t('components.manage.moderation.InstancePolicyCard.label.blockAll') }}
</p> </p>
<div <div
@ -49,7 +51,7 @@ const summary = useMarkdown(() => props.object.summary)
v-if="object.silence_activity" v-if="object.silence_activity"
class="ui item" class="ui item"
> >
<i class="feed icon" /> <i class="bi bi-rss-fill" />
<div class="content"> <div class="content">
{{ t('components.manage.moderation.InstancePolicyCard.label.muteActivity') }} {{ t('components.manage.moderation.InstancePolicyCard.label.muteActivity') }}
</div> </div>
@ -58,7 +60,7 @@ const summary = useMarkdown(() => props.object.summary)
v-if="object.silence_notifications" v-if="object.silence_notifications"
class="ui item" class="ui item"
> >
<i class="bell icon" /> <i class="bi bi-bell-fill" />
<div class="content"> <div class="content">
{{ t('components.manage.moderation.InstancePolicyCard.label.muteNotifications') }} {{ t('components.manage.moderation.InstancePolicyCard.label.muteNotifications') }}
</div> </div>
@ -67,7 +69,7 @@ const summary = useMarkdown(() => props.object.summary)
v-if="object.reject_media" v-if="object.reject_media"
class="ui item" class="ui item"
> >
<i class="file icon" /> <i class="bi bi-file-earmark-fill" />
<div class="content"> <div class="content">
{{ t('components.manage.moderation.InstancePolicyCard.label.rejectMedia') }} {{ t('components.manage.moderation.InstancePolicyCard.label.rejectMedia') }}
</div> </div>
@ -80,12 +82,12 @@ const summary = useMarkdown(() => props.object.summary)
<sanitized-html :html="summary" /> <sanitized-html :html="summary" />
</div> </div>
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<button <Button
class="ui right floated labeled icon button" destructive
icon="bi-pencil"
@click="emit('update')" @click="emit('update')"
> >
<i class="edit icon" />
{{ t('components.manage.moderation.InstancePolicyCard.button.edit') }} {{ t('components.manage.moderation.InstancePolicyCard.button.edit') }}
</button> </Button>
</div> </div>
</template> </template>

View File

@ -9,6 +9,9 @@ import axios from 'axios'
import DangerousButton from '~/components/common/DangerousButton.vue' import DangerousButton from '~/components/common/DangerousButton.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
interface Events { interface Events {
(e: 'save', data: InstancePolicy): void (e: 'save', data: InstancePolicy): void
(e: 'delete'): void (e: 'delete'): void
@ -119,8 +122,8 @@ const remove = async () => {
</script> </script>
<template> <template>
<form <Layout
class="ui form" form
@submit.prevent="createOrUpdate" @submit.prevent="createOrUpdate"
> >
<h3 class="ui header"> <h3 class="ui header">
@ -135,10 +138,9 @@ const remove = async () => {
{{ t('components.manage.moderation.InstancePolicyForm.header.addRule') }} {{ t('components.manage.moderation.InstancePolicyForm.header.addRule') }}
</span> </span>
</h3> </h3>
<div <Alert
v-if="errors && errors.length > 0" v-if="errors && errors.length > 0"
role="alert" red
class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ t('components.manage.moderation.InstancePolicyForm.header.failure') }} {{ t('components.manage.moderation.InstancePolicyForm.header.failure') }}
@ -151,7 +153,7 @@ const remove = async () => {
{{ error }} {{ error }}
</li> </li>
</ul> </ul>
</div> </Alert>
<div <div
v-if="object" v-if="object"
@ -220,37 +222,39 @@ const remove = async () => {
</label> </label>
</div> </div>
</div> </div>
<div class="ui hidden divider" /> <Layout flex>
<button <Button
class="ui basic left floated button" primary
@click.prevent="emit('cancel')" @click.prevent="emit('cancel')"
> >
{{ t('components.manage.moderation.InstancePolicyForm.button.cancel') }} {{ t('components.manage.moderation.InstancePolicyForm.button.cancel') }}
</button> </Button>
<button <Button
:class="['ui', 'right', 'floated', 'success', {'disabled loading': isLoading}, 'button']" primary
:disabled="isLoading" :class="{'disabled loading': isLoading}"
> :disabled="isLoading"
<span v-if="object"> >
{{ t('components.manage.moderation.InstancePolicyForm.button.update') }} <span v-if="object">
</span> {{ t('components.manage.moderation.InstancePolicyForm.button.update') }}
<span v-else> </span>
{{ t('components.manage.moderation.InstancePolicyForm.button.create') }} <span v-else>
</span> {{ t('components.manage.moderation.InstancePolicyForm.button.create') }}
</button> </span>
<dangerous-button </Button>
v-if="object" <dangerous-button
style="float: right;" v-if="object"
:title="t('components.manage.moderation.InstancePolicyForm.modal.delete.header')" style="float: right;"
@confirm="remove" :title="t('components.manage.moderation.InstancePolicyForm.modal.delete.header')"
> @confirm="remove"
{{ t('components.manage.moderation.InstancePolicyForm.button.delete') }} >
<template #modal-content> {{ t('components.manage.moderation.InstancePolicyForm.button.delete') }}
{{ t('components.manage.moderation.InstancePolicyForm.modal.delete.content.warning') }} <template #modal-content>
</template> {{ t('components.manage.moderation.InstancePolicyForm.modal.delete.content.warning') }}
<template #modal-confirm> </template>
{{ t('components.manage.moderation.InstancePolicyForm.button.confirm') }} <template #modal-confirm>
</template> {{ t('components.manage.moderation.InstancePolicyForm.button.confirm') }}
</dangerous-button> </template>
</form> </dangerous-button>
</Layout>
</Layout>
</template> </template>

View File

@ -5,6 +5,8 @@ import axios from 'axios'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Button from '~/components/ui/Button.vue'
interface Events { interface Events {
(e: 'created', note: Note): void (e: 'created', note: Note): void
} }
@ -76,12 +78,13 @@ const submit = async () => {
:placeholder="labels.summaryPlaceholder" :placeholder="labels.summaryPlaceholder"
/> />
</div> </div>
<button <Button
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" primary
:class="[{'loading': isLoading}, 'right', 'floated']"
type="submit" type="submit"
:disabled="isLoading" :disabled="isLoading"
> >
{{ t('components.manage.moderation.NoteForm.button.add') }} {{ t('components.manage.moderation.NoteForm.button.add') }}
</button> </Button>
</form> </form>
</template> </template>

View File

@ -11,6 +11,10 @@ import useErrorHandler from '~/composables/useErrorHandler'
import DangerousButton from '~/components/common/DangerousButton.vue' import DangerousButton from '~/components/common/DangerousButton.vue'
import Alert from '~/components/ui/Alert.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/Spacer.vue'
interface Events { interface Events {
(e: 'deleted', uuid: string): void (e: 'deleted', uuid: string): void
} }
@ -40,48 +44,53 @@ const remove = async (note: Note) => {
<template> <template>
<div class="ui feed"> <div class="ui feed">
<div <Alert
v-for="note in notes" v-for="note in notes"
:key="note.uuid" :key="note.uuid"
blue
class="event" class="event"
> >
<div class="label"> <div class="label">
<i class="comment outline icon" /> <i
class="bi bi-chat-dots-fill"
style="font-size: 22px;"
/>
</div> </div>
<div class="content"> <Spacer :size="16" />
<div class="summary"> <Layout
<actor-link flex
:admin="true" gap-16
:actor="note.author" class="summary"
/> >
<div class="date"> <actor-link
<human-date :date="note.creation_date" /> :admin="true"
</div> :actor="note.author"
/>
<div class="date">
<human-date :date="note.creation_date" />
</div> </div>
<div class="extra text"> </Layout>
<expandable-div :content="note.summary"> <expandable-div :content="note.summary">
<sanitized-html :html="useMarkdownRaw(note.summary ?? '')" /> <sanitized-html :html="useMarkdownRaw(note.summary ?? '')" />
</expandable-div> </expandable-div>
</div> <template #actions>
<div class="meta"> <dangerous-button
<dangerous-button :is-loading="isLoading"
:is-loading="isLoading" low-height
low-height icon="bi-trash"
icon="bi-trash" :title="t('components.manage.moderation.NotesThread.modal.delete.header')"
:title="t('components.manage.moderation.NotesThread.modal.delete.header')" @confirm="remove(note)"
@confirm="remove(note)" >
> {{ t('components.manage.moderation.NotesThread.button.delete') }}
{{ t('components.manage.moderation.NotesThread.button.delete') }}
<template #modal-content> <template #modal-content>
{{ t('components.manage.moderation.NotesThread.modal.delete.content.warning') }} {{ t('components.manage.moderation.NotesThread.modal.delete.content.warning') }}
</template> </template>
<template #modal-confirm> <template #modal-confirm>
{{ t('components.manage.moderation.NotesThread.button.delete') }} {{ t('components.manage.moderation.NotesThread.button.delete') }}
</template> </template>
</dangerous-button> </dangerous-button>
</div> </template>
</div> </Alert>
</div>
</div> </div>
</template> </template>

View File

@ -18,6 +18,11 @@ import useErrorHandler from '~/composables/useErrorHandler'
import useMarkdown from '~/composables/useMarkdown' import useMarkdown from '~/composables/useMarkdown'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import Card from '~/components/ui/Card.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
interface Events { interface Events {
(e: 'updated', updating: { type: string }): void (e: 'updated', updating: { type: string }): void
(e: 'handled', isHandled: boolean): void (e: 'handled', isHandled: boolean): void
@ -110,7 +115,7 @@ const update = async (type: string) => {
} }
const store = useStore() const store = useStore()
const isCollapsed = ref(false) const isCollapsed = ref(true)
const resolveReport = async (isHandled: boolean) => { const resolveReport = async (isHandled: boolean) => {
isLoading.value = true isLoading.value = true
@ -140,136 +145,124 @@ const handleRemovedNote = (uuid: string) => {
</script> </script>
<template> <template>
<div class="ui fluid report card"> <Card
<div class="content"> :title="t('components.manage.moderation.ReportCard.link.report', {id: obj.uuid.substring(0, 8)})"
<h4 class="header"> :width="isCollapsed ? '350px' : ''"
<router-link :to="{name: 'manage.moderation.reports.detail', params: {id: obj.uuid}}"> :full="!isCollapsed"
{{ t('components.manage.moderation.ReportCard.link.report', {id: obj.uuid.substring(0, 8)}) }} solid
</router-link> :red="!obj.is_handled && isCollapsed"
<collapse-link :green="obj.is_handled && isCollapsed"
v-model="isCollapsed" >
class="right floated" <template #topright>
/> <collapse-link
</h4> v-model="isCollapsed"
<div class="content"> class="right floated"
<div class="ui hidden divider" /> />
<div class="ui stackable two column grid"> </template>
<div class="column"> <table class="ui very basic unstackable table">
<table class="ui very basic unstackable table"> <tbody>
<tbody> <tr>
<tr> <td>
<td> {{ t('components.manage.moderation.ReportCard.table.report.submittedBy') }}
{{ t('components.manage.moderation.ReportCard.table.report.submittedBy') }} </td>
</td> <td>
<td> <div v-if="obj.submitter">
<div v-if="obj.submitter"> <actor-link
<actor-link :admin="true"
:admin="true" :actor="obj.submitter"
:actor="obj.submitter" />
/> </div>
</div> <div v-else-if="obj.submitter_email">
<div v-else-if="obj.submitter_email"> {{ obj.submitter_email }}
{{ obj.submitter_email }} </div>
</div> </td>
</td> </tr>
</tr> <tr>
<tr> <td>
<td> {{ t('components.manage.moderation.ReportCard.table.report.category') }}
{{ t('components.manage.moderation.ReportCard.table.report.category') }} </td>
</td> <td>
<td> <report-category-dropdown
<report-category-dropdown v-model="obj.type"
v-model="obj.type" @update:model-value="update($event)"
@update:model-value="update($event)" >
> &#32;
&#32; <action-feedback :is-loading="updating.type" />
<action-feedback :is-loading="updating.type" /> </report-category-dropdown>
</report-category-dropdown> </td>
</td> </tr>
</tr> <tr>
<tr> <td>
<td> {{ t('components.manage.moderation.ReportCard.table.report.creationDate') }}
{{ t('components.manage.moderation.ReportCard.table.report.creationDate') }} </td>
</td> <td>
<td> <human-date
<human-date :date="obj.creation_date"
:date="obj.creation_date" :icon="true"
:icon="true" />
/> </td>
</td> </tr>
</tr> <tr>
</tbody> <td>
</table> {{ t('components.manage.moderation.ReportCard.table.status.status') }}
</div> </td>
<div class="column"> <td v-if="obj.is_handled">
<table class="ui very basic unstackable table"> <span v-if="obj.is_handled">
<tbody> <i class="success check icon" />
<tr> {{ t('components.manage.moderation.ReportCard.table.status.resolved') }}
<td> </span>
{{ t('components.manage.moderation.ReportCard.table.status.status') }} </td>
</td> <td v-else>
<td v-if="obj.is_handled"> <i class="danger x icon" />
<span v-if="obj.is_handled"> {{ t('components.manage.moderation.ReportCard.table.status.unresolved') }}
<i class="success check icon" /> </td>
{{ t('components.manage.moderation.ReportCard.table.status.resolved') }} </tr>
</span> <tr>
</td> <td>
<td v-else> {{ t('components.manage.moderation.ReportCard.table.status.assignedTo') }}
<i class="danger x icon" /> </td>
{{ t('components.manage.moderation.ReportCard.table.status.unresolved') }} <td>
</td> <div v-if="obj.assigned_to">
</tr> <actor-link
<tr> :admin="true"
<td> :actor="obj.assigned_to"
{{ t('components.manage.moderation.ReportCard.table.status.assignedTo') }} />
</td> </div>
<td> <span v-else>
<div v-if="obj.assigned_to"> {{ t('components.manage.moderation.ReportCard.notApplicable') }}
<actor-link </span>
:admin="true" </td>
:actor="obj.assigned_to" </tr>
/> <tr>
</div> <td>
<span v-else> {{ t('components.manage.moderation.ReportCard.table.status.resolutionDate') }}
{{ t('components.manage.moderation.ReportCard.notApplicable') }} </td>
</span> <td>
</td> <human-date
</tr> v-if="obj.handled_date"
<tr> :date="obj.handled_date"
<td> :icon="true"
{{ t('components.manage.moderation.ReportCard.table.status.resolutionDate') }} />
</td> <span v-else>
<td> {{ t('components.manage.moderation.ReportCard.notApplicable') }}
<human-date </span>
v-if="obj.handled_date" </td>
:date="obj.handled_date" </tr>
:icon="true" <tr>
/> <td>
<span v-else> {{ t('components.manage.moderation.ReportCard.table.status.internalNotes') }}
{{ t('components.manage.moderation.ReportCard.notApplicable') }} </td>
</span> <td>
</td> <i class="comment icon" />
</tr> {{ obj.notes.length }}
<tr> </td>
<td> </tr>
{{ t('components.manage.moderation.ReportCard.table.status.internalNotes') }} </tbody>
</td> </table>
<td> <template
<i class="comment icon" />
{{ obj.notes.length }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div
v-if="!isCollapsed" v-if="!isCollapsed"
class="main content"
> >
<div class="ui stackable two column grid"> <Layout flex>
<div class="column"> <div class="column">
<h3> <h3>
{{ t('components.manage.moderation.ReportCard.header.message') }} {{ t('components.manage.moderation.ReportCard.header.message') }}
@ -286,29 +279,34 @@ const handleRemovedNote = (uuid: string) => {
<h3> <h3>
{{ t('components.manage.moderation.ReportCard.header.reportedObject') }} {{ t('components.manage.moderation.ReportCard.header.reportedObject') }}
</h3> </h3>
<div <Alert
v-if="!obj.target" v-if="!obj.target"
role="alert" red
class="ui warning message"
> >
{{ t('components.manage.moderation.ReportCard.warning.objectDeleted') }} {{ t('components.manage.moderation.ReportCard.warning.objectDeleted') }}
</div> </Alert>
<router-link <Layout flex>
v-if="target && configs[target.type].urls.getDetail" <Link
class="ui basic button" v-if="target && configs[target.type].urls.getDetail"
:to="configs[target.type].urls.getDetail?.(obj.target_state) ?? '/'" solid
> secondary
<i class="eye icon" /> icon="bi-eye"
{{ t('components.manage.moderation.ReportCard.link.publicPage') }} low-height
</router-link> :to="configs[target.type].urls.getDetail?.(obj.target_state) ?? '/'"
<router-link >
v-if="target && configs[target.type].urls.getAdminDetail" {{ t('components.manage.moderation.ReportCard.link.publicPage') }}
class="ui basic button" </Link>
:to="configs[target.type].urls.getAdminDetail?.(obj.target_state) ?? '/'" <Link
> v-if="target && configs[target.type].urls.getAdminDetail"
<i class="wrench icon" /> solid
{{ t('components.manage.moderation.ReportCard.link.moderation') }} secondary
</router-link> icon="bi-wrench"
low-height
:to="configs[target.type].urls.getAdminDetail?.(obj.target_state) ?? '/'"
>
{{ t('components.manage.moderation.ReportCard.link.moderation') }}
</Link>
</Layout>
<table class="ui very basic unstackable table"> <table class="ui very basic unstackable table">
<tbody> <tbody>
<tr v-if="target"> <tr v-if="target">
@ -369,9 +367,9 @@ const handleRemovedNote = (uuid: string) => {
</tr> </tr>
<tr v-else-if="obj.target_state.domain"> <tr v-else-if="obj.target_state.domain">
<td> <td>
<router-link :to="{name: 'manage.moderation.domains.detail', params: { id: obj.target_state.domain }}"> <Link :to="{name: 'manage.moderation.domains.detail', params: { id: obj.target_state.domain }}">
{{ t('components.manage.moderation.ReportCard.table.object.domain') }} {{ t('components.manage.moderation.ReportCard.table.object.domain') }}
</router-link> </Link>
</td> </td>
<td> <td>
{{ obj.target_state.domain }} {{ obj.target_state.domain }}
@ -405,65 +403,61 @@ const handleRemovedNote = (uuid: string) => {
</tbody> </tbody>
</table> </table>
</aside> </aside>
</div> </Layout>
<div class="ui stackable two column grid"> <h3>
<div class="column"> {{ t('components.manage.moderation.ReportCard.header.notes') }}
<h3> </h3>
{{ t('components.manage.moderation.ReportCard.header.notes') }} <notes-thread
</h3> :notes="obj.notes"
<notes-thread @deleted="handleRemovedNote($event)"
:notes="obj.notes" />
@deleted="handleRemovedNote($event)" <note-form
/> :target="{type: 'report', uuid: obj.uuid}"
<note-form @created="obj.notes.push($event)"
:target="{type: 'report', uuid: obj.uuid}" />
@created="obj.notes.push($event)" <h3>
/> {{ t('components.manage.moderation.ReportCard.header.actions') }}
</div> </h3>
<div class="column"> <Layout flex>
<h3> <Button
{{ t('components.manage.moderation.ReportCard.header.actions') }} v-if="obj.is_handled === false"
</h3> :class="{loading: isLoading}"
<div class="ui labelled icon basic buttons"> primary
<button icon="bi-check"
v-if="obj.is_handled === false" @click="resolveReport(true)"
:class="['ui', {loading: isLoading}, 'button']" >
@click="resolveReport(true)" {{ t('components.manage.moderation.ReportCard.button.resolve') }}
> </Button>
<i class="success check icon" />&nbsp; <Button
{{ t('components.manage.moderation.ReportCard.button.resolve') }} v-if="obj.is_handled === true"
</button> :class="{loading: isLoading}"
<button secondary
v-if="obj.is_handled === true" icon="bi-arrow-counterclockwise"
:class="['ui', {loading: isLoading}, 'button']" @click="resolveReport(false)"
@click="resolveReport(false)" >
> {{ t('components.manage.moderation.ReportCard.button.unresolve') }}
<i class="warning redo icon" />&nbsp; </Button>
{{ t('components.manage.moderation.ReportCard.button.unresolve') }} <template
</button> v-for="action in actions"
<template :key="action.label"
v-for="action in actions" >
:key="action.label" <dangerous-button
> v-if="action.dangerous && action.show(obj)"
<dangerous-button :is-loading="isLoading"
v-if="action.dangerous && action.show(obj)" :action="action.handler"
:is-loading="isLoading" :title="action.modalHeader"
:action="action.handler" :icon="`${action.iconColor} ${action.icon}`"
:title="action.modalHeader" >
:icon="`${action.iconColor} ${action.icon}`" {{ action.label }}
> <template #modal-content>
{{ action.label }} {{ action.modalContent }}
<template #modal-content>
{{ action.modalContent }}
</template>
<template #modal-confirm>
{{ action.modalConfirmLabel }}
</template>
</dangerous-button>
</template> </template>
</div> <template #modal-confirm>
</div> {{ action.modalConfirmLabel }}
</div> </template>
</div> </dangerous-button>
</div> </template>
</Layout>
</template>
</Card>
</template> </template>

View File

@ -11,6 +11,7 @@ import useLogger from '~/composables/useLogger'
import Modal from '~/components/ui/Modal.vue' import Modal from '~/components/ui/Modal.vue'
import Alert from '~/components/ui/Alert.vue' import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue'
const logger = useLogger() const logger = useLogger()
const { t } = useI18n() const { t } = useI18n()
@ -62,6 +63,7 @@ const hide = async () => {
<template> <template>
<Modal <Modal
v-model="show" v-model="show"
destructive
:title="type==='artist' ? t('components.moderation.FilterModal.header.modal', {name: target?.name}) : errors.length > 0 ? t('components.moderation.FilterModal.header.failure') : ''" :title="type==='artist' ? t('components.moderation.FilterModal.header.modal', {name: target?.name}) : errors.length > 0 ? t('components.moderation.FilterModal.header.failure') : ''"
:cancel="t('components.moderation.FilterModal.button.cancel')" :cancel="t('components.moderation.FilterModal.button.cancel')"
> >
@ -104,13 +106,14 @@ const hide = async () => {
</template> </template>
</div> </div>
</div> </div>
<div class="actions"> <template #actions>
<button <Button
:class="['ui', 'success', {loading: isLoading}, 'button']" destructive
:class="[{loading: isLoading}]"
@click="hide" @click="hide"
> >
{{ t('components.moderation.FilterModal.button.hide') }} {{ t('components.moderation.FilterModal.button.hide') }}
</button> </Button>
</div> </template>
</Modal> </Modal>
</template> </template>

View File

@ -58,11 +58,11 @@ const allCategories = computed(() => {
</script> </script>
<template> <template>
<div> <div class="ui form">
<label v-if="label">{{ t('components.moderation.ReportCategoryDropdown.label.category') }}</label> <label v-if="label">{{ t('components.moderation.ReportCategoryDropdown.label.category') }}</label>
<select <select
v-model="value" v-model="value"
class="ui dropdown" class="dropdown"
:required="required || undefined" :required="required || undefined"
> >
<option <option

View File

@ -133,10 +133,17 @@ const moderationNotifications = computed(() =>
icon="bi-megaphone-fill" icon="bi-megaphone-fill"
> >
{{ t('components.Sidebar.link.moderation') }} {{ t('components.Sidebar.link.moderation') }}
<Spacer grow />
<div <div
v-if="store.state.ui.notifications.pendingReviewReports + store.state.ui.notifications.pendingReviewRequests > 0" v-if="store.state.ui.notifications.pendingReviewReports + store.state.ui.notifications.pendingReviewRequests > 0"
:title="t('components.Sidebar.label.reports')" :title="t('components.Sidebar.label.reports')"
:class="['ui', 'circular', 'mini', 'right floated', 'accent', 'label']" style="
background: var(--fw-gray-400);
color: var(--fw-gray-800);
padding: 2px 7px;
border-radius: 10px;
font-size: 12px;
"
> >
{{ store.state.ui.notifications.pendingReviewReports + store.state.ui.notifications.pendingReviewRequests }} {{ store.state.ui.notifications.pendingReviewReports + store.state.ui.notifications.pendingReviewRequests }}
</div> </div>

View File

@ -97,6 +97,7 @@ const labels = computed(() => ({
color: var(--fw-gray-800); color: var(--fw-gray-800);
padding: 2px 7px; padding: 2px 7px;
border-radius: 10px; border-radius: 10px;
font-size: 12px;
" "
> >
{{ store.state.ui.notifications.inbox }} {{ store.state.ui.notifications.inbox }}

View File

@ -36,31 +36,29 @@ const labels = computed(() => ({
const isLoading = ref(false) const isLoading = ref(false)
const object = ref() const object = ref()
const fetchData = async () => { const fetchData = async () => {
isLoading.value = true isLoading.value = true
try { try {
const response = await axios.get(`manage/library/libraries/${props.id}/`) const response = await axios.get(`manage/library/libraries/${props.id}/`)
object.value = response.data object.value = response.data
} catch (error) { } catch (error) {
useErrorHandler(error as Error) useErrorHandler(error as Error)
} }
isLoading.value = false isLoading.value = false
} }
const isLoadingStats = ref(false) const isLoadingStats = ref(false)
const stats = ref() const stats = ref()
const fetchStats = async () => { const fetchStats = async () => {
isLoadingStats.value = true isLoadingStats.value = true
try { try {
const response = await axios.get(`manage/library/libraries/${props.id}/stats/`) const response = await axios.get(`manage/library/libraries/${props.id}/stats/`)
stats.value = response.data stats.value = response.data
} catch (error) { } catch (error) {
useErrorHandler(error as Error) useErrorHandler(error as Error)
} }
isLoadingStats.value = false isLoadingStats.value = false
} }
@ -69,14 +67,12 @@ fetchData()
const remove = async () => { const remove = async () => {
isLoading.value = true isLoading.value = true
try { try {
await axios.delete(`manage/library/libraries/${props.id}/`) await axios.delete(`manage/library/libraries/${props.id}/`)
router.push({ name: 'manage.library.libraries' }) router.push({ name: 'manage.library.libraries' })
} catch (error) { } catch (error) {
useErrorHandler(error as Error) useErrorHandler(error as Error)
} }
isLoading.value = false isLoading.value = false
} }
@ -86,7 +82,6 @@ const updateObj = async (attr: string) => {
const params = { const params = {
[attr]: object.value[attr] [attr]: object.value[attr]
} }
try { try {
await axios.patch(`manage/library/libraries/${props.id}/`, params) await axios.patch(`manage/library/libraries/${props.id}/`, params)
logger.info(`${attr} was updated successfully to ${params[attr]}`) logger.info(`${attr} was updated successfully to ${params[attr]}`)

View File

@ -337,7 +337,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
</router-link> </router-link>
</Layout> </Layout>
<Layout <Layout
v-if="!track.is_local" v-if="!track?.is_local"
flex flex
class="details" class="details"
> >
@ -345,7 +345,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
class="label" class="label"
:to="{ :to="{
name: 'manage.moderation.domains.detail', name: 'manage.moderation.domains.detail',
params: { id: track.domain } params: { id: track?.domain }
}" }"
> >
{{ t('views.admin.library.TrackDetail.link.domain') }} {{ t('views.admin.library.TrackDetail.link.domain') }}
@ -354,7 +354,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
h h
grow grow
/> />
<span class="value">{{ track.domain }}</span> <span class="value">{{ track?.domain }}</span>
</Layout> </Layout>
<Layout <Layout
v-if="track?.description" v-if="track?.description"
@ -428,7 +428,10 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
<span class="value">{{ stats?.track_favorites }}</span> <span class="value">{{ stats?.track_favorites }}</span>
</Layout> </Layout>
</Layout> </Layout>
<Layout stack> <Layout
stack
style="flex: 1; gap: 0;"
>
<Heading <Heading
:h3="t('views.admin.library.TrackDetail.header.trackData')" :h3="t('views.admin.library.TrackDetail.header.trackData')"
class="category" class="category"
@ -444,7 +447,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
h h
grow grow
/> />
<span class="value">{{ humanSize(stats.media_downloaded_size) }}</span> <span class="value">{{ humanSize(stats?.media_downloaded_size) }}</span>
</Layout> </Layout>
<Layout <Layout
flex flex
@ -457,7 +460,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
h h
grow grow
/> />
<span class="value">{{ humanSize(stats.media_total_size) }}</span> <span class="value">{{ humanSize(stats?.media_total_size) }}</span>
</Layout> </Layout>
<Layout <Layout
flex flex
@ -465,7 +468,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
> >
<Link <Link
class="label" class="label"
:to="{ name: 'manage.library.libraries', query: { q: getQuery('track_id', track.id) } }" :to="{ name: 'manage.library.libraries', query: { q: getQuery('track_id', track?.id) } }"
> >
{{ t('views.admin.library.TrackDetail.link.libraries') }} {{ t('views.admin.library.TrackDetail.link.libraries') }}
</Link> </Link>
@ -473,7 +476,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
h h
grow grow
/> />
<span class="value">{{ stats.libraries }}</span> <span class="value">{{ stats?.libraries }}</span>
</Layout> </Layout>
<Layout <Layout
flex flex
@ -481,7 +484,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
> >
<Link <Link
class="label" class="label"
:to="{ name: 'manage.library.uploads', query: { q: getQuery('track_id', track.id) } }" :to="{ name: 'manage.library.uploads', query: { q: getQuery('track_id', track?.id) } }"
> >
{{ t('views.admin.library.TrackDetail.link.uploads') }} {{ t('views.admin.library.TrackDetail.link.uploads') }}
</Link> </Link>
@ -489,7 +492,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
h h
grow grow
/> />
<span class="value">{{ stats.uploads }}</span> <span class="value">{{ stats?.uploads }}</span>
</Layout> </Layout>
</Layout> </Layout>
</Layout> </Layout>

View File

@ -2,17 +2,21 @@
import type { InstancePolicy } from '~/types' import type { InstancePolicy } from '~/types'
import { computed, ref, reactive } from 'vue' import { computed, ref, reactive } from 'vue'
// import { useCurrentElement } from '@vueuse/core'
import { humanSize } from '~/utils/filters' import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
import Header from '~/components/ui/Header.vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Loader from '~/components/ui/Loader.vue'
import Button from '~/components/ui/Button.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
import HumanDate from '~/components/common/HumanDate.vue'
import Link from '~/components/ui/Link.vue'
import Button from '~/components/ui/Button.vue'
import Heading from '~/components/ui/Heading.vue'
import Loader from '~/components/ui/Loader.vue'
import Alert from '~/components/ui/Alert.vue'
import Input from '~/components/ui/Input.vue' import Input from '~/components/ui/Input.vue'
import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue' import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue'
@ -104,13 +108,6 @@ const fetchStats = async () => {
fetchStats() fetchStats()
fetchData() fetchData()
// TODO: Find out if the following removal causes any regression #2440
// const el = useCurrentElement()
// watch(object, async () => {
// await nextTick()
// $(el.value).find('select.dropdown').dropdown()
// })
const getQuery = (field: string, value: string) => `${field}:"${value}"` const getQuery = (field: string, value: string) => `${field}:"${value}"`
const updating = reactive(new Set<string>()) const updating = reactive(new Set<string>())
@ -152,466 +149,427 @@ const updatePolicy = (newPolicy: InstancePolicy) => {
</script> </script>
<template> <template>
<Layout <Loader v-if="isLoading" />
main <Header
stack v-if="object"
class="page-admin-account-detail" v-title="object?.full_username"
:h1="object?.full_username"
page-heading
> >
<Loader <template #image>
v-if="isLoading" <i class="channel-image bi-person-circle" />
/>
<template v-if="object">
<section
v-title="object.full_username"
>
<Layout flex>
<h2 class="ui header">
<i class="bi-person-circle" />
{{ object.full_username }}
<div class="sub header">
<template v-if="object.user">
<span class="ui tiny accent label">
<i class="bi-house-fill" />
{{ t('views.admin.moderation.AccountsDetail.header.localAccount') }}
</span>
&nbsp;
</template>
<a
:href="object.url || object.fid"
target="_blank"
rel="noopener noreferrer"
>
{{ t('views.admin.moderation.AccountsDetail.link.openProfile') }}&nbsp;
<i class="bi bi-box-arrow-up-right" />
</a>
</div>
</h2>
<Spacer grow />
<Layout stack>
<a
v-if="object.user && store.state.auth.profile && store.state.auth.profile.is_superuser"
class="ui labeled icon button"
:href="store.getters['instance/absoluteUrl'](`/api/admin/users/user/${object.user.id}`)"
target="_blank"
rel="noopener noreferrer"
>
<i class="bi bi-wrench" />
{{ t('views.admin.moderation.AccountsDetail.link.django') }}&nbsp;
</a>
<a
v-else-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
:href="store.getters['instance/absoluteUrl'](`/api/admin/federation/actor/${object.id}`)"
target="_blank"
rel="noopener noreferrer"
>
<i class="bi bi-wrench" />
{{ t('views.admin.moderation.AccountsDetail.link.django') }}&nbsp;
</a>
<a
:href="object.url || object.fid"
target="_blank"
rel="noopener noreferrer"
>
<i class="bi bi-box-arrow-up-right" />
{{ t('views.admin.moderation.AccountsDetail.link.remoteProfile') }}&nbsp;
</a>
</Layout>
<div class="ui column">
<div
v-if="!object.user"
class="ui compact clearing placeholder segment component-placeholder"
>
<template v-if="isLoadingPolicy">
<div class="paragraph">
<div class="line" />
<div class="line" />
<div class="line" />
<div class="line" />
<div class="line" />
</div>
</template>
<template v-else-if="!policy && !showPolicyForm">
<header class="ui header">
<h3>
<i class="bi bi-shield-fill" />
{{ t('views.admin.moderation.AccountsDetail.header.noPolicy') }}
</h3>
</header>
<p>
{{ t('views.admin.moderation.AccountsDetail.description.policy') }}
</p>
<Button
primary
@click="showPolicyForm = true"
>
{{ t('views.admin.moderation.AccountsDetail.button.addPolicy') }}
</Button>
</template>
<instance-policy-card
v-else-if="policy && !showPolicyForm"
:object="policy"
@update="showPolicyForm = true"
>
<header class="ui header">
<h3>
{{ t('views.admin.moderation.AccountsDetail.header.activePolicy') }}
</h3>
</header>
</instance-policy-card>
<instance-policy-form
v-else-if="showPolicyForm"
:object="policy"
type="actor"
:target="object.full_username"
@cancel="showPolicyForm = false"
@save="updatePolicy"
@delete="policy = null; showPolicyForm = false"
/>
</div>
</div>
</Layout>
</section>
<Layout flex>
<div class="column">
<section>
<h3 class="ui header">
<i class="bi bi-info-circle-fill" />
{{ t('views.admin.moderation.AccountsDetail.header.accountData') }}
</h3>
<table class="ui very basic table">
<tbody>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.username') }}
</td>
<td>
{{ object.preferred_username }}
</td>
</tr>
<tr v-if="!object.user">
<td>
<router-link :to="{name: 'manage.moderation.domains.detail', params: {id: object.domain }}">
{{ t('views.admin.moderation.AccountsDetail.link.domain') }}
</router-link>
</td>
<td>
{{ object.domain }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.displayName') }}
</td>
<td>
{{ object.name }}
</td>
</tr>
<tr v-if="object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.email') }}
</td>
<td>
{{ object.user.email }}
</td>
</tr>
<tr v-if="object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.loginStatus.label') }}
</td>
<td>
<div
v-if="object.user.username != store.state.auth.profile?.username"
class="ui toggle checkbox"
>
<Input
id="is-active"
v-model="object.user.is_active"
type="checkbox"
@change="updateUser('is_active')"
/>
<label for="is-active">
<span
v-if="object.user.is_active"
>{{ t('views.admin.moderation.AccountsDetail.table.accountData.loginStatus.enabled') }}</span>
<span
v-else
>{{ t('views.admin.moderation.AccountsDetail.table.accountData.loginStatus.disabled') }}</span>
</label>
</div>
<span
v-else-if="object.user.is_active"
>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.loginStatus.enabled') }}
</span>
<span
v-else
>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.loginStatus.disabled') }}
</span>
</td>
</tr>
<tr v-if="object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.permissions') }}
</td>
<td>
<select
v-model="permissions"
multiple
class="ui search selection dropdown"
@change="updateUser('permissions')"
>
<option
v-for="(p, key) in allPermissions"
:key="key"
:value="p.code"
>
{{ p.label }}
</option>
</select>
<action-feedback :is-loading="updating.has('permissions')" />
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.userType') }}
</td>
<td>
{{ object.type }}
</td>
</tr>
<tr v-if="!object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.lastChecked') }}
</td>
<td>
<human-date
v-if="object.last_fetch_date"
:date="object.last_fetch_date"
/>
<span
v-else
>
{{ t('views.admin.moderation.AccountsDetail.notApplicable') }}
</span>
</td>
</tr>
<tr v-if="object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.signupDate') }}
</td>
<td>
<human-date :date="object.user.date_joined" />
</td>
</tr>
<tr v-if="object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.accountData.lastActivity') }}
</td>
<td>
<human-date :date="object.user.last_activity" />
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="column">
<section>
<h3 class="ui header">
<i class="bi bi-rss-fill" />
{{ t('views.admin.moderation.AccountsDetail.header.activity') }}&nbsp;
<span :data-tooltip="labels.statsWarning"><i class=" bi bi-question-circle-fill" /></span>
</h3>
<div
v-if="isLoadingStats"
class="ui placeholder"
>
<div class="full line" />
<div class="short line" />
<div class="medium line" />
<div class="long line" />
</div>
<table
v-else
class="ui very basic table"
>
<tbody>
<tr v-if="!object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.activity.firstSeen') }}
</td>
<td>
<human-date :date="object.creation_date" />
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.table.activity.emittedMessages') }}
</td>
<td>
{{ stats.outbox_activities }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.table.activity.receivedFollows') }}
</td>
<td>
{{ stats.received_library_follows }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.table.activity.emittedFollows') }}
</td>
<td>
{{ stats.emitted_library_follows }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `account:${object.full_username}`) }}">
{{ t('views.admin.moderation.AccountsDetail.link.linkedReports') }}
</router-link>
</td>
<td>
{{ stats.reports }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.moderation.requests.list', query: {q: getQuery('submitter', `${object.full_username}`) }}">
{{ t('views.admin.moderation.AccountsDetail.link.requests') }}
</router-link>
</td>
<td>
{{ stats.requests }}
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="column">
<section>
<h3 class="ui header">
<i class="bi bi-music-note-beamed" />
{{ t('views.admin.moderation.AccountsDetail.header.audioContent') }}&nbsp;
<span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span>
</h3>
<div
v-if="isLoadingStats"
class="ui placeholder"
>
<div class="full line" />
<div class="short line" />
<div class="medium line" />
<div class="long line" />
</div>
<table
v-else
class="ui very basic table"
>
<tbody>
<tr v-if="!object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.cachedSize') }}
</td>
<td>
{{ humanSize(stats.media_downloaded_size) }}
</td>
</tr>
<tr v-if="object.user">
<td>
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.uploadQuota') }}
<span :data-tooltip="labels.uploadQuota"><i class="question circle icon" /></span>
</td>
<td>
<Input
v-model.number="object.user.upload_quota"
step="100"
name="quota"
type="number"
@change="updateUser('upload_quota', true)"
/>
<div class="ui basic label">
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.megabyte') }}
</div>
<action-feedback
class="ui basic label"
size="tiny"
:is-loading="updating.has('upload_quota')"
/>
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.totalSize') }}
</td>
<td>
{{ humanSize(stats.media_total_size) }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.channels', query: {q: getQuery('account', object.full_username) }}">
{{ t('views.admin.moderation.AccountsDetail.link.channels') }}
</router-link>
</td>
<td>
{{ stats.channels }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('account', object.full_username) }}">
{{ t('views.admin.moderation.AccountsDetail.link.libraries') }}
</router-link>
</td>
<td>
{{ stats.libraries }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('account', object.full_username) }}">
{{ t('views.admin.moderation.AccountsDetail.link.uploads') }}
</router-link>
</td>
<td>
{{ stats.uploads }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.link.artists') }}
</td>
<td>
{{ stats.artists }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.link.albums') }}
</td>
<td>
{{ stats.albums }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.AccountsDetail.link.tracks') }}
</td>
<td>
{{ stats.tracks }}
</td>
</tr>
</tbody>
</table>
</section>
</div>
</Layout>
</template> </template>
<Spacer />
<Layout
flex
class="header-buttons"
>
<Link
solid
primary
low-height
icon="bi-box-arrow-up-right"
:to="object?.url || object?.fid"
target="_blank"
>
{{ t('views.admin.moderation.AccountsDetail.link.openProfile') }}
</Link>
<Link
v-if="object?.user && store.state.auth.profile?.is_superuser"
solid
primary
low-height
icon="bi-wrench"
:to="store.getters['instance/absoluteUrl'](`/api/admin/users/user/${object?.user?.id}`)"
target="_blank"
>
{{ t('views.admin.moderation.AccountsDetail.link.django') }}
</Link>
<Link
v-if="!object?.user"
solid
primary
low-height
:to="store.getters['instance/absoluteUrl'](`/api/admin/federation/actor/${object?.id}`)"
icon="bi-wrench"
target="_blank"
>
{{ t('views.admin.moderation.AccountsDetail.link.django') }}
</Link>
</Layout>
</Header>
<Alert
v-if="!object?.user"
:blue="!policy"
red="policy"
>
<template v-if="isLoadingPolicy">
<div class="paragraph">
<div class="line" />
<div class="line" />
<div class="line" />
<div class="line" />
<div class="line" />
</div>
</template>
<template v-else-if="!policy && !showPolicyForm">
<Heading
:h3="t('views.admin.moderation.AccountsDetail.header.noPolicy')"
icon="bi-shield-fill"
/>
<p>
{{ t('views.admin.moderation.AccountsDetail.description.policy') }}
</p>
<Button
primary
@click="showPolicyForm = true"
>
{{ t('views.admin.moderation.AccountsDetail.button.addPolicy') }}
</Button>
</template>
<instance-policy-card
v-else-if="policy && !showPolicyForm"
:object="policy"
@update="showPolicyForm = true"
>
<header class="ui header">
<h3>
{{ t('views.admin.moderation.AccountsDetail.header.activePolicy') }}
</h3>
</header>
</instance-policy-card>
<instance-policy-form
v-else-if="showPolicyForm"
:object="policy"
type="actor"
:target="object?.full_username"
@cancel="showPolicyForm = false"
@save="updatePolicy"
@delete="policy = null; showPolicyForm = false"
/>
</Alert>
<Spacer />
<Layout
flex
gap-64
>
<Layout
stack
style="flex: 1; gap: 0;"
>
<Heading
:h3="t('views.admin.moderation.AccountsDetail.header.accountData')"
class="category"
/>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.accountData.username') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ object?.preferred_username }}</span>
</Layout>
<Layout
v-if="!object?.user"
flex
class="details"
>
<router-link
class="label"
:to="{ name: 'manage.moderation.domains.detail', params: { id: object?.domain } }"
>
{{ t('views.admin.moderation.AccountsDetail.link.domain') }}
</router-link>
<Spacer
h
grow
/>
<span class="value">{{ object?.domain }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.accountData.displayName') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ object?.name }}</span>
</Layout>
<Layout
v-if="object?.user"
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.accountData.email') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ object?.user?.email }}</span>
</Layout>
<Layout
v-if="object?.user"
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.accountData.signupDate') }}
</span>
<Spacer
h
grow
/>
<HumanDate :date="object?.user?.date_joined" />
</Layout>
</Layout>
<Layout
stack
style="flex: 1; gap: 0;"
>
<Heading
:h3="t('views.admin.moderation.AccountsDetail.header.activity')"
class="category"
>
<span
:data-tooltip="labels.statsWarning"
style="margin-left: 8px"
>
<i class=" bi bi-question-circle-fill" /></span>
</Heading>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.activity.firstSeen') }}
</span>
<Spacer
h
grow
/>
<HumanDate :date="object?.creation_date" />
</Layout>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.activity.emittedMessages') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.outbox_activities }}</span>
</Layout>
<Layout
v-if="object?.user"
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.activity.receivedFollows') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.received_library_follows }}</span>
</Layout>
<Layout
v-if="object?.user"
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.activity.emittedFollows') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.emitted_library_follows }}</span>
</Layout>
<Layout
v-if="object?.user"
flex
class="details"
>
<router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `account:${object?.full_username}`) }}">
{{ t('views.admin.moderation.AccountsDetail.link.linkedReports') }}
</router-link>
<Spacer
h
grow
/>
<span class="value">{{ stats?.reports }}</span>
</Layout>
<Layout
v-if="object?.user"
flex
class="details"
>
<router-link :to="{name: 'manage.moderation.requests.list', query: {q: getQuery('submitter', `${object?.full_username}`) }}">
{{ t('views.admin.moderation.AccountsDetail.link.requests') }}
</router-link>
<Spacer
h
grow
/>
<span class="value">{{ stats?.requests }}</span>
</Layout>
</Layout>
<Layout
stack
style="flex: 1; gap: 0;"
>
<Heading
:h3="t('views.admin.moderation.AccountsDetail.header.audioContent')"
class="category"
>
<span
:data-tooltip="labels.statsWarning"
style="margin-left: 8px"
>
<i class=" bi bi-question-circle-fill" /></span>
</Heading>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.cachedSize') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ humanSize(stats?.media_downloaded_size) }}</span>
</Layout>
<Layout
v-if="object?.user"
flex
no-gap
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.uploadQuota') }}
</span>
<span
:data-tooltip="labels.uploadQuota"
style="margin-left: 8px"
>
<i class="bi bi-question-circle" />
</span>
<Spacer
h
grow
/>
<Input
v-model.number="object.user.upload_quota"
step="100"
name="quota"
type="number"
style="width: 100px"
@change="updateUser('upload_quota', true)"
/>
<span class="ui basic label">
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.megabyte') }}
</span>
<action-feedback
class="ui basic label"
size="tiny"
:is-loading="updating.has('upload_quota')"
/>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.AccountsDetail.table.audioContent.totalSize') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ humanSize(stats?.media_total_size) }}</span>
</Layout>
</Layout>
</Layout> </Layout>
</template> </template>
<style scoped lang="scss">
.channel-image {
width: 200px;
height: 200px;
font-size: 160px;
border: none;
display: block;
text-align: center;
align-content: center;
border-radius: 50%;
@include light-theme {
background-color: var(--fw-gray-200);
}
@include dark-theme {
background-color: var(--fw-gray-800);
}
}
h3.category {
margin-bottom: 16px;
}
.details {
padding: 0 16px;
height: 72px;
align-items: center;
border-top: 1px solid;
min-width: 280px;
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}
.label {
font-weight: 800;
@include light-theme {
color: var(--fw-gray-600);
}
@include dark-theme {
color: var(--fw-gray-500);
}
}
a.label,
a.value {
text-decoration: underline;
}
&:last-child {
border-bottom: 1px solid;
}
}
</style>

View File

@ -14,6 +14,19 @@ import InstancePolicyCard from '~/components/manage/moderation/InstancePolicyCar
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import Header from '~/components/ui/Header.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/Spacer.vue'
import HumanDate from '~/components/common/HumanDate.vue'
import Link from '~/components/ui/Link.vue'
import Button from '~/components/ui/Button.vue'
import Heading from '~/components/ui/Heading.vue'
import Loader from '~/components/ui/Loader.vue'
import Alert from '~/components/ui/Alert.vue'
import OptionsButton from '~/components/ui/button/Options.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
interface Props { interface Props {
id: number id: number
allowListEnabled: boolean allowListEnabled: boolean
@ -109,398 +122,442 @@ const setAllowList = async (value: boolean) => {
</script> </script>
<template> <template>
<main class="page-admin-domain-detail"> <Loader v-if="isLoading" />
<div <Header
v-if="isLoading" v-if="object"
class="ui vertical segment" v-title="object?.name"
:h1="object?.name"
page-heading
>
<template #image>
<i class="channel-image bi bi-cloud-fill" />
</template>
<Spacer />
<Layout
flex
class="header-buttons"
> >
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" /> <Link
</div> solid
<template v-if="object"> primary
<section low-height
v-title="object.name" icon="bi-box-arrow-up-right"
:class="['ui', 'head', 'vertical', 'stripe', 'segment']" :to="externalUrl"
target="_blank"
> >
<div class="ui stackable two column grid"> {{ t('views.admin.moderation.DomainsDetail.link.website') }}
<div class="ui column"> </Link>
<div class="segment-content"> <Link
<h2 class="ui header"> v-if="store.state.auth.profile?.is_superuser"
<i class="circular inverted cloud icon" /> solid
<div class="content"> primary
{{ object.name }} low-height
<div class="sub header"> icon="bi-wrench"
<a :to="store.getters['instance/absoluteUrl'](`/api/admin/federation/domain/${object.name}`)"
:href="externalUrl" target="_blank"
target="_blank" >
rel="noopener noreferrer" {{ t('views.admin.moderation.DomainsDetail.link.django') }}
class="logo-wrapper" </Link>
> <Spacer grow />
{{ t('views.admin.moderation.DomainsDetail.link.website') }}&nbsp; <Popover v-if="allowListEnabled">
<i class="external icon" /> <template #default="{ toggleOpen }">
</a> <OptionsButton
</div> is-square-small
</div> @click="toggleOpen()"
</h2> />
<div class="header-buttons"> </template>
<div class="ui icon buttons"> <template #items>
<a <PopoverItem
v-if="store.state.auth.profile?.is_superuser" icon="bi-list-check"
class="ui labeled icon button" @click="setAllowList(!object.allowed)"
:href="store.getters['instance/absoluteUrl'](`/api/admin/federation/domain/${object.name}`)" >
target="_blank" <span v-if="object.allowed">
rel="noopener noreferrer" {{ t('views.admin.moderation.DomainsDetail.button.removeFromAllowList') }}
> </span>
<i class="wrench icon" /> <span v-else>
{{ t('views.admin.moderation.DomainsDetail.link.django') }}&nbsp; {{ t('views.admin.moderation.DomainsDetail.button.addToAllowList') }}
</a> </span>
</div> </PopoverItem>
<div </template>
v-if="allowListEnabled" </Popover>
class="ui icon buttons" </Layout>
> </Header>
<button
v-if="object.allowed"
:class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']"
@click.prevent="setAllowList(false)"
>
<i class="x icon" />
{{ t('views.admin.moderation.DomainsDetail.button.removeFromAllowList') }}
</button>
<button
v-else
:class="['ui', 'labeled', {loading: isLoadingAllowList}, 'icon', 'button']"
@click.prevent="setAllowList(true)"
>
<i class="check icon" />
{{ t('views.admin.moderation.DomainsDetail.button.addToAllowList') }}
</button>
</div>
</div>
</div>
</div>
<div class="ui column">
<div class="ui compact clearing placeholder segment component-placeholder">
<template v-if="isLoadingPolicy">
<div class="paragraph">
<div class="line" />
<div class="line" />
<div class="line" />
<div class="line" />
<div class="line" />
</div>
</template>
<template v-else-if="!policy && !showPolicyForm">
<header class="ui header">
<h3>
<i class="shield icon" />
{{ t('views.admin.moderation.DomainsDetail.header.noPolicy') }}
</h3>
</header>
<p>
{{ t('views.admin.moderation.DomainsDetail.description.policy') }}
</p>
<button
class="ui primary button"
@click="showPolicyForm = true"
>
{{ t('views.admin.moderation.DomainsDetail.button.addPolicy') }}
</button>
</template>
<instance-policy-card
v-else-if="policy && !showPolicyForm"
:object="policy"
@update="showPolicyForm = true"
>
<header class="ui header">
<h3>
{{ t('views.admin.moderation.DomainsDetail.header.activePolicy') }}
</h3>
</header>
</instance-policy-card>
<instance-policy-form
v-else-if="showPolicyForm"
:object="policy"
type="domain"
:target="object.name"
@cancel="showPolicyForm = false"
@save="updatePolicy"
@delete="policy = null; showPolicyForm = false"
/>
</div>
</div>
</div>
</section>
<div class="ui vertical stripe segment">
<div class="ui stackable three column grid">
<div class="column">
<section>
<h3 class="ui header">
<i class="info icon" />
<div class="content">
{{ t('views.admin.moderation.DomainsDetail.header.instanceData') }}
</div>
</h3>
<table class="ui very basic table">
<tbody>
<tr v-if="allowListEnabled">
<td>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.inAllowList.label') }}
</td>
<td>
<span
v-if="object.allowed"
>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.inAllowList.true') }}
</span>
<span
v-else
>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.inAllowList.false') }}
</span>
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.lastChecked') }}
</td>
<td>
<human-date
v-if="object.nodeinfo_fetch_date"
:date="object.nodeinfo_fetch_date"
/>
<span
v-else
>
{{ t('views.admin.moderation.DomainsDetail.notApplicable') }}
</span>
</td>
</tr>
<template v-if="object.nodeinfo && object.nodeinfo.status === 'ok'"> <Alert
<tr> blue
<td> >
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.software.label') }} <template v-if="isLoadingPolicy">
</td> <div class="paragraph">
<td> <div class="line" />
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.software.value', {name: get(object, 'nodeinfo.payload.software.name', t('views.admin.moderation.DomainsDetail.notApplicable')), version: get(object, 'nodeinfo.payload.software.version', t('views.admin.moderation.DomainsDetail.notApplicable'))}) }} <div class="line" />
</td> <div class="line" />
</tr> <div class="line" />
<tr> <div class="line" />
<td>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.domainName') }}
</td>
<td>
{{ get(object, 'nodeinfo.payload.metadata.nodeName', t('views.admin.moderation.DomainsDetail.notApplicable')) }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.totalUsers') }}
</td>
<td>
{{ get(object, 'nodeinfo.payload.usage.users.total', t('views.admin.moderation.DomainsDetail.notApplicable')) }}
</td>
</tr>
</template>
<template v-if="object.nodeinfo && object.nodeinfo.status === 'error'">
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.nodeInfoStatus.label') }}
</td>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.nodeInfoStatus.value') }}&nbsp;
<span :data-tooltip="object.nodeinfo.error"><i class="question circle icon" /></span>
</td>
</tr>
</template>
</tbody>
</table>
<ajax-button
method="get"
:url="'manage/federation/domains/' + object.name + '/nodeinfo/'"
@action-done="refreshNodeInfo"
>
{{ t('views.admin.moderation.DomainsDetail.button.refreshNodeInfo') }}
</ajax-button>
</section>
</div>
<div class="column">
<section>
<h3 class="ui header">
<i class="feed icon" />
<div class="content">
{{ t('views.admin.moderation.DomainsDetail.header.activity') }}&nbsp;
<span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span>
</div>
</h3>
<div
v-if="isLoadingStats"
class="ui placeholder"
>
<div class="full line" />
<div class="short line" />
<div class="medium line" />
<div class="long line" />
</div>
<table
v-else
class="ui very basic table"
>
<tbody>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.activity.firstSeen') }}
</td>
<td>
<human-date :date="object.creation_date" />
</td>
</tr>
<tr>
<td>
<router-link
:to="{name: 'manage.moderation.accounts.list', query: {q: 'domain:' + object.name }}"
>
{{ t('views.admin.moderation.DomainsDetail.link.knownAccounts') }}
</router-link>
</td>
<td>
{{ stats.actors }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.activity.emittedMessages') }}
</td>
<td>
{{ stats.outbox_activities }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.activity.receivedFollows') }}
</td>
<td>
{{ stats.received_library_follows }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.activity.emittedFollows') }}
</td>
<td>
{{ stats.emitted_library_follows }}
</td>
</tr>
</tbody>
</table>
</section>
</div>
<div class="column">
<section>
<h3 class="ui header">
<i class="music icon" />
<div class="content">
{{ t('views.admin.moderation.DomainsDetail.header.audioContent') }}&nbsp;
<span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span>
</div>
</h3>
<div
v-if="isLoadingStats"
class="ui placeholder"
>
<div class="full line" />
<div class="short line" />
<div class="medium line" />
<div class="long line" />
</div>
<table
v-else
class="ui very basic table"
>
<tbody>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.audioContent.cachedSize') }}
</td>
<td>
{{ humanSize(stats.media_downloaded_size) }}
</td>
</tr>
<tr>
<td>
{{ t('views.admin.moderation.DomainsDetail.table.audioContent.totalSize') }}
</td>
<td>
{{ humanSize(stats.media_total_size) }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.channels', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.channels') }}
</router-link>
</td>
<td>
{{ stats.channels }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.libraries') }}
</router-link>
</td>
<td>
{{ stats.libraries }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.uploads') }}
</router-link>
</td>
<td>
{{ stats.uploads }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.artists', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.artists') }}
</router-link>
</td>
<td>
{{ stats.artists }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.albums', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.albums') }}
</router-link>
</td>
<td>
{{ stats.albums }}
</td>
</tr>
<tr>
<td>
<router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.tracks') }}
</router-link>
</td>
<td>
{{ stats.tracks }}
</td>
</tr>
</tbody>
</table>
</section>
</div>
</div>
</div> </div>
</template> </template>
</main> <template v-else-if="!policy && !showPolicyForm">
<Heading
:h3="t('views.admin.moderation.DomainsDetail.header.noPolicy')"
icon="bi-shield-lock"
/>
<p>
{{ t('views.admin.moderation.DomainsDetail.description.policy') }}
</p>
<Button
primary
@click="showPolicyForm = true"
>
{{ t('views.admin.moderation.DomainsDetail.button.addPolicy') }}
</Button>
</template>
<instance-policy-card
v-else-if="policy && !showPolicyForm"
:object="policy"
@update="showPolicyForm = true"
>
<header class="ui header">
<h3>
{{ t('views.admin.moderation.DomainsDetail.header.activePolicy') }}
</h3>
</header>
</instance-policy-card>
<instance-policy-form
v-else-if="showPolicyForm"
:object="policy"
type="domain"
:target="object.name"
@cancel="showPolicyForm = false"
@save="updatePolicy"
@delete="policy = null; showPolicyForm = false"
/>
</Alert>
<Spacer />
<Layout
flex
gap-64
>
<Layout
stack
style="flex: 1; gap: 0;"
>
<Heading
:h3="t('views.admin.moderation.DomainsDetail.header.instanceData')"
class="category"
/>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.inAllowList.label') }}
</span>
<Spacer
h
grow
/>
<span class="value">
<span v-if="object?.allowed">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.inAllowList.true') }}
</span>
<span v-else>
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.inAllowList.false') }}
</span>
</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.lastChecked') }}
</span>
<Spacer
h
grow
/>
<HumanDate
v-if="object?.nodeinfo_fetch_date"
:date="object?.nodeinfo_fetch_date"
/>
<span v-else>
{{ t('views.admin.moderation.DomainsDetail.notApplicable') }}
</span>
</Layout>
<Layout
v-if="object?.nodeinfo && object?.nodeinfo.status === 'ok'"
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.software.label') }}
</span>
<Spacer
h
grow
/>
<span class="value">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.software.value', {name: get(object, 'nodeinfo.payload.software.name', t('views.admin.moderation.DomainsDetail.notApplicable')), version: get(object, 'nodeinfo.payload.software.version', t('views.admin.moderation.DomainsDetail.notApplicable'))}) }}
</span>
</Layout>
<Layout
v-if="object?.nodeinfo && object?.nodeinfo.status === 'error'"
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.nodeInfoStatus.label') }}
</span>
<Spacer
h
grow
/>
<span class="value">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.nodeInfoStatus.value', {name: get(object, 'nodeinfo.payload.software.name', t('views.admin.moderation.DomainsDetail.notApplicable')), version: get(object, 'nodeinfo.payload.software.version', t('views.admin.moderation.DomainsDetail.notApplicable'))}) }}
</span>
<span :data-tooltip="object.nodeinfo.error"><i class="bi bi-question-circle" /></span>
</Layout>
<ajax-button
method="get"
:url="'manage/federation/domains/' + object?.name + '/nodeinfo/'"
@action-done="refreshNodeInfo"
>
{{ t('views.admin.moderation.DomainsDetail.button.refreshNodeInfo') }}
</ajax-button>
</Layout>
<Layout
stack
style="flex: 1; gap: 0;"
>
<Heading
:h3="t('views.admin.moderation.DomainsDetail.header.activity')"
class="category"
>
<span
:data-tooltip="labels.statsWarning"
style="margin-left: 8px;"
>
<i class="bi bi-question-circle" /></span>
</Heading>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.DomainsDetail.table.activity.firstSeen') }}
</span>
<Spacer
h
grow
/>
<HumanDate :date="object?.creation_date" />
</Layout>
<Layout
flex
class="details"
>
<Link
class="label"
:to="{name: 'manage.moderation.accounts.list', query: {q: 'domain:' + object?.name }}"
>
{{ t('views.admin.moderation.DomainsDetail.link.knownAccounts') }}
</Link>
<Spacer
h
grow
/>
<span class="value">{{ stats?.actors }}</span>
</Layout>
</Layout>
<Layout
stack
style="flex: 1; gap: 0;"
>
<Heading
:h3="t('views.admin.moderation.DomainsDetail.header.audioContent')"
class="category"
>
<span
:data-tooltip="labels.statsWarning"
style="margin-left: 8px;"
>
<i class="bi bi-question-circle" /></span>
</Heading>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.DomainsDetail.table.audioContent.cachedSize') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ humanSize(stats?.media_downloaded_size) }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
{{ t('views.admin.moderation.DomainsDetail.table.audioContent.totalSize') }}
</span>
<Spacer
h
grow
/>
<span class="value">{{ humanSize(stats?.media_total_size) }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.channels', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.channels') }}
</router-link>
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.channels }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.libraries') }}
</router-link>
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.libraries }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.uploads') }}
</router-link>
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.uploads }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.artists', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.artists') }}
</router-link>
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.artists }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.albums', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.albums') }}
</router-link>
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.albums }}</span>
</Layout>
<Layout
flex
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('domain', object.name) }}">
{{ t('views.admin.moderation.DomainsDetail.link.tracks') }}
</router-link>
</span>
<Spacer
h
grow
/>
<span class="value">{{ stats?.tracks }}</span>
</Layout>
</Layout>
</Layout>
</template> </template>
<style scoped lang="scss">
.channel-image {
width: 200px;
height: 200px;
font-size: 160px;
border: none;
display: block;
text-align: center;
align-content: center;
@include light-theme {
background-color: var(--fw-gray-200);
}
@include dark-theme {
background-color: var(--fw-gray-800);
}
}
h3.category {
margin-bottom: 16px;
}
.details {
padding: 0 16px;
height: 72px;
align-items: center;
border-top: 1px solid;
min-width: 280px;
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}
.label {
font-weight: 800;
@include light-theme {
color: var(--fw-gray-600);
}
@include dark-theme {
color: var(--fw-gray-500);
}
}
a.label,
a.value {
text-decoration: underline;
}
&:last-child {
border-bottom: 1px solid;
}
}
</style>

View File

@ -26,6 +26,7 @@ import useOrdering from '~/composables/navigation/useOrdering'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import usePage from '~/composables/navigation/usePage' import usePage from '~/composables/navigation/usePage'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import Loader from '~/components/ui/Loader.vue'
interface Props extends SmartSearchProps, OrderingProps { interface Props extends SmartSearchProps, OrderingProps {
mode?: 'card' mode?: 'card'
@ -194,12 +195,9 @@ const labels = computed(() => ({
</Layout> </Layout>
</div> </div>
</div> </div>
<div <Loader
v-if="isLoading" v-if="isLoading"
class="ui active inverted dimmer" />
>
<div class="ui loader" />
</div>
<div v-else-if="!result || result.count === 0"> <div v-else-if="!result || result.count === 0">
<empty-state <empty-state
:refresh="true" :refresh="true"