diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 7a450e4f50c07276a9c44fd6cf5aa4b6110ef0bf..48e5982da3f52b380c4debec0c1cb5043aea5f99 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -62,9 +62,32 @@ class ActorQuerySet(models.QuerySet): return qs +class DomainQuerySet(models.QuerySet): + def external(self): + return self.exclude(pk=settings.FEDERATION_HOSTNAME) + + def with_last_activity_date(self): + activities = Activity.objects.filter( + actor__domain=models.OuterRef("pk") + ).order_by("-creation_date") + + return self.annotate( + last_activity_date=models.Subquery(activities.values("creation_date")[:1]) + ) + + def with_actors_count(self): + return self.annotate(actors_count=models.Count("actors", distinct=True)) + + def with_outbox_activities_count(self): + return self.annotate( + outbox_activities_count=models.Count("actors__outbox_activities") + ) + + class Domain(models.Model): name = models.CharField(primary_key=True, max_length=255) creation_date = models.DateTimeField(default=timezone.now) + objects = DomainQuerySet.as_manager() def __str__(self): return self.name diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 4347b4cc42089a0376577d98dbbf9097e37dd419..d9b9bfc1df0a1e079260095aab74b2c73cb8db75 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -1,6 +1,7 @@ from django_filters import rest_framework as filters from funkwhale_api.common import fields +from funkwhale_api.federation import models as federation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models @@ -20,6 +21,14 @@ class ManageUploadFilterSet(filters.FilterSet): fields = ["q", "track__album", "track__artist", "track"] +class ManageDomainFilterSet(filters.FilterSet): + q = fields.SearchFilter(search_fields=["name"]) + + class Meta: + model = federation_models.Domain + fields = ["name"] + + class ManageUserFilterSet(filters.FilterSet): q = fields.SearchFilter(search_fields=["username", "email", "name"]) diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 9b5e24f662d9f25c39e980108b119f85e67c8470..8686a99b92abbc788cc63473358727fc66d26d5e 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -3,6 +3,7 @@ from django.db import transaction from rest_framework import serializers from funkwhale_api.common import serializers as common_serializers +from funkwhale_api.federation import models as federation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models @@ -168,3 +169,28 @@ class ManageInvitationActionSerializer(common_serializers.ActionSerializer): @transaction.atomic def handle_delete(self, objects): return objects.delete() + + +class ManageDomainSerializer(serializers.ModelSerializer): + actors_count = serializers.SerializerMethodField() + last_activity_date = serializers.SerializerMethodField() + outbox_activities_count = serializers.SerializerMethodField() + + class Meta: + model = federation_models.Domain + fields = [ + "name", + "creation_date", + "actors_count", + "last_activity_date", + "outbox_activities_count", + ] + + def get_actors_count(self, o): + return getattr(o, "actors_count", 0) + + def get_last_activity_date(self, o): + return getattr(o, "last_activity_date", None) + + def get_outbox_activities_count(self, o): + return getattr(o, "outbox_activities_count", 0) diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index 9f5503978c435797d012fc0595a8c8b50aa81568..26832f946e5430198a5804030241e9efaa8a6461 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -3,6 +3,8 @@ from rest_framework import routers from . import views +federation_router = routers.SimpleRouter() +federation_router.register(r"domains", views.ManageDomainViewSet, "domains") library_router = routers.SimpleRouter() library_router.register(r"uploads", views.ManageUploadViewSet, "uploads") users_router = routers.SimpleRouter() @@ -10,6 +12,10 @@ users_router.register(r"users", views.ManageUserViewSet, "users") users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations") urlpatterns = [ + url( + r"^federation/", + include((federation_router.urls, "federation"), namespace="federation"), + ), url(r"^library/", include((library_router.urls, "instance"), namespace="library")), url(r"^users/", include((users_router.urls, "instance"), namespace="users")), ] diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index bfd5b2ef21bb3c1854ed711bb52b57f5bdc6a079..30f7179e885e6dbedc7c25bd8d28811405ce1aa5 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -2,6 +2,7 @@ from rest_framework import mixins, response, viewsets from rest_framework.decorators import list_route from funkwhale_api.common import preferences +from funkwhale_api.federation import models as federation_models from funkwhale_api.music import models as music_models from funkwhale_api.users import models as users_models from funkwhale_api.users.permissions import HasUserPermission @@ -92,3 +93,26 @@ class ManageInvitationViewSet( serializer.is_valid(raise_exception=True) result = serializer.save() return response.Response(result, status=200) + + +class ManageDomainViewSet( + mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + queryset = ( + federation_models.Domain.objects.external() + .with_last_activity_date() + .with_actors_count() + .with_outbox_activities_count() + .order_by("name") + ) + serializer_class = serializers.ManageDomainSerializer + filter_class = filters.ManageDomainFilterSet + permission_classes = (HasUserPermission,) + required_permissions = ["moderation"] + ordering_fields = [ + "name", + "creation_date", + "last_activity_date", + "actors_count", + "outbox_activities_count", + ] diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index 18966430e21bd9ccc9ab4637a6178971b0f10804..c3032817c455f0c19062c4ab17d9db89b93d39f2 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -1,6 +1,8 @@ import pytest from django import db +from funkwhale_api.federation import models + def test_cannot_duplicate_actor(factories): actor = factories["federation.Actor"]() @@ -67,3 +69,11 @@ def test_actor_get_quota(factories): def test_domain_name_saved_properly(value, expected, factories): domain = factories["federation.Domain"](name=value) assert domain.name == expected + + +def test_external_domains(factories, settings): + d1 = factories["federation.Domain"]() + d2 = factories["federation.Domain"]() + settings.FEDERATION_HOSTNAME = d1.pk + + assert list(models.Domain.objects.external()) == [d2] diff --git a/api/tests/manage/test_filters.py b/api/tests/manage/test_filters.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 2b46ee839c9d53752f8a481f54461a43bfe9214c..be02e6727a2b61a2492c3248da355c95d9907441 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -21,7 +21,7 @@ def test_user_update_permission(factories): user, data={ "is_active": False, - "permissions": {"federation": False, "upload": True}, + "permissions": {"moderation": True, "settings": False}, "upload_quota": 12, }, ) @@ -33,4 +33,21 @@ def test_user_update_permission(factories): assert user.upload_quota == 12 assert user.permission_moderation is True assert user.permission_library is False - assert user.permission_settings is True + assert user.permission_settings is False + + +def test_manage_domain_serializer(factories, now): + domain = factories["federation.Domain"]() + setattr(domain, "actors_count", 42) + setattr(domain, "outbox_activities_count", 23) + setattr(domain, "last_activity_date", now) + expected = { + "name": domain.name, + "creation_date": domain.creation_date.isoformat().split("+")[0] + "Z", + "last_activity_date": now, + "actors_count": 42, + "outbox_activities_count": 23, + } + s = serializers.ManageDomainSerializer(domain) + + assert s.data == expected diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index a9920ce0761b655074cba3a1a7e18104fea8ca49..3d153073a906252273292c0881d4a2583a72aa1d 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -10,6 +10,7 @@ from funkwhale_api.manage import serializers, views (views.ManageUploadViewSet, ["library"], "and"), (views.ManageUserViewSet, ["settings"], "and"), (views.ManageInvitationViewSet, ["settings"], "and"), + (views.ManageDomainViewSet, ["moderation"], "and"), ], ) def test_permissions(assert_user_permission, view, permissions, operator): @@ -64,3 +65,15 @@ def test_invitation_view_create(factories, superuser_api_client, mocker): assert response.status_code == 201 assert superuser_api_client.user.invitations.latest("id") is not None + + +def test_domain_list(factories, superuser_api_client, settings): + factories["federation.Domain"](pk=settings.FEDERATION_HOSTNAME) + d = factories["federation.Domain"]() + url = reverse("api:v1:manage:federation:domains-list") + response = superuser_api_client.get(url) + + assert response.status_code == 200 + + assert response.data["count"] == 1 + assert response.data["results"][0]["name"] == d.pk diff --git a/api/tests/radios/test_filters.py b/api/tests/radios/test_filters.py deleted file mode 100644 index 89bb726aff643c7365b759b510f763b738c738d0..0000000000000000000000000000000000000000 --- a/api/tests/radios/test_filters.py +++ /dev/null @@ -1,153 +0,0 @@ -import pytest -from django.core.exceptions import ValidationError - -from funkwhale_api.music.models import Track -from funkwhale_api.radios import filters - - -@filters.registry.register -class NoopFilter(filters.RadioFilter): - code = "noop" - - def get_query(self, candidates, **kwargs): - return - - -def test_most_simple_radio_does_not_filter_anything(factories): - factories["music.Track"].create_batch(3) - radio = factories["radios.Radio"](config=[{"type": "noop"}]) - - assert radio.version == 0 - assert radio.get_candidates().count() == 3 - - -def test_filter_can_use_custom_queryset(factories): - tracks = factories["music.Track"].create_batch(3) - candidates = Track.objects.filter(pk=tracks[0].pk) - - qs = filters.run([{"type": "noop"}], candidates=candidates) - assert qs.count() == 1 - assert qs.first() == tracks[0] - - -def test_filter_on_tag(factories): - tracks = factories["music.Track"].create_batch(3, tags=["metal"]) - factories["music.Track"].create_batch(3, tags=["pop"]) - expected = tracks - f = [{"type": "tag", "names": ["metal"]}] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == expected - - -def test_filter_on_artist(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - factories["music.Track"].create_batch(3, artist=artist1) - factories["music.Track"].create_batch(3, artist=artist2) - expected = list(artist1.tracks.order_by("pk")) - f = [{"type": "artist", "ids": [artist1.pk]}] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == expected - - -def test_can_combine_with_or(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - artist3 = factories["music.Artist"]() - factories["music.Track"].create_batch(3, artist=artist1) - factories["music.Track"].create_batch(3, artist=artist2) - factories["music.Track"].create_batch(3, artist=artist3) - expected = Track.objects.exclude(artist=artist3).order_by("pk") - f = [ - {"type": "artist", "ids": [artist1.pk]}, - {"type": "artist", "ids": [artist2.pk], "operator": "or"}, - ] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_can_combine_with_and(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - metal_tracks = factories["music.Track"].create_batch( - 2, artist=artist1, tags=["metal"] - ) - factories["music.Track"].create_batch(2, artist=artist1, tags=["pop"]) - factories["music.Track"].create_batch(3, artist=artist2) - expected = metal_tracks - f = [ - {"type": "artist", "ids": [artist1.pk]}, - {"type": "tag", "names": ["metal"], "operator": "and"}, - ] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_can_negate(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - factories["music.Track"].create_batch(3, artist=artist1) - factories["music.Track"].create_batch(3, artist=artist2) - expected = artist2.tracks.order_by("pk") - f = [{"type": "artist", "ids": [artist1.pk], "not": True}] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_can_group(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - factories["music.Track"].create_batch(2, artist=artist1) - t1 = factories["music.Track"].create_batch(2, artist=artist1, tags=["metal"]) - factories["music.Track"].create_batch(2, artist=artist2) - t2 = factories["music.Track"].create_batch(2, artist=artist2, tags=["metal"]) - factories["music.Track"].create_batch(2, tags=["metal"]) - expected = t1 + t2 - f = [ - {"type": "tag", "names": ["metal"]}, - { - "type": "group", - "operator": "and", - "filters": [ - {"type": "artist", "ids": [artist1.pk], "operator": "or"}, - {"type": "artist", "ids": [artist2.pk], "operator": "or"}, - ], - }, - ] - - candidates = filters.run(f) - assert list(candidates.order_by("pk")) == list(expected) - - -def test_artist_filter_clean_config(factories): - artist1 = factories["music.Artist"]() - artist2 = factories["music.Artist"]() - - config = filters.clean_config({"type": "artist", "ids": [artist2.pk, artist1.pk]}) - - expected = { - "type": "artist", - "ids": [artist1.pk, artist2.pk], - "names": [artist1.name, artist2.name], - } - assert filters.clean_config(config) == expected - - -def test_can_check_artist_filter(factories): - artist = factories["music.Artist"]() - - assert filters.validate({"type": "artist", "ids": [artist.pk]}) - with pytest.raises(ValidationError): - filters.validate({"type": "artist", "ids": [artist.pk + 1]}) - - -def test_can_check_operator(): - assert filters.validate({"type": "group", "operator": "or", "filters": []}) - assert filters.validate({"type": "group", "operator": "and", "filters": []}) - with pytest.raises(ValidationError): - assert filters.validate({"type": "group", "operator": "nope", "filters": []}) diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index f072ce808fb3dc9cb7e8f0f3f2e3b924bdc2ed73..3a5bf2db8a3ddbee831bbbdf5d88c7fc1d018b3f 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -76,19 +76,27 @@ class="item" :to="{name: 'content.index'}"><i class="upload icon"></i><translate>Add content</translate></router-link> </div> </div> - <div class="item" v-if="$store.state.auth.availablePermissions['settings']"> + <div class="item" v-if="$store.state.auth.availablePermissions['settings'] || $store.state.auth.availablePermissions['moderation']"> <header class="header"><translate>Administration</translate></header> <div class="menu"> <router-link + v-if="$store.state.auth.availablePermissions['settings']" class="item" :to="{path: '/manage/settings'}"> <i class="settings icon"></i><translate>Settings</translate> </router-link> <router-link + v-if="$store.state.auth.availablePermissions['settings']" class="item" :to="{name: 'manage.users.users.list'}"> <i class="users icon"></i><translate>Users</translate> </router-link> + <router-link + v-if="$store.state.auth.availablePermissions['moderation']" + class="item" + :to="{name: 'manage.moderation.domains.list'}"> + <i class="shield icon"></i><translate>Moderation</translate> + </router-link> </div> </div> </nav> diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue index e8dec339aaa85d85172af1da44c7c11d8b4dc7b0..5b138a3c6f909d51b39352561f20b8419aae57d7 100644 --- a/front/src/components/common/ActionTable.vue +++ b/front/src/components/common/ActionTable.vue @@ -1,7 +1,7 @@ <template> <table class="ui compact very basic single line unstackable table"> <thead> - <tr v-if="actions.length > 0"> + <tr v-if="actionUrl && actions.length > 0"> <th colspan="1000"> <div class="ui small form"> <div class="ui inline fields"> @@ -130,8 +130,8 @@ import axios from 'axios' export default { props: { - actionUrl: {type: String, required: true}, - idField: {type: String, required: true, default: 'id'}, + actionUrl: {type: String, required: false, default: null}, + idField: {type: String, required: false, default: 'id'}, objectsData: {type: Object, required: true}, actions: {type: Array, required: true, default: () => { return [] }}, filters: {type: Object, required: false, default: () => { return {} }}, diff --git a/front/src/components/manage/moderation/DomainsTable.vue b/front/src/components/manage/moderation/DomainsTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..35cd96d6939dbe0a53509143243865c06aaf6b8b --- /dev/null +++ b/front/src/components/manage/moderation/DomainsTable.vue @@ -0,0 +1,190 @@ +<template> + <div> + <div class="ui inline form"> + <div class="fields"> + <div class="ui field"> + <label><translate>Search</translate></label> + <input type="text" v-model="search" :placeholder="labels.searchPlaceholder" /> + </div> + <div class="field"> + <label><translate>Ordering</translate></label> + <select class="ui dropdown" v-model="ordering"> + <option v-for="option in orderingOptions" :value="option[0]"> + {{ sharedLabels.filters[option[1]] }} + </option> + </select> + </div> + <div class="field"> + <label><translate>Ordering direction</translate></label> + <select class="ui dropdown" v-model="orderingDirection"> + <option value="+"><translate>Ascending</translate></option> + <option value="-"><translate>Descending</translate></option> + </select> + </div> + </div> + </div> + <div class="dimmable"> + <div v-if="isLoading" class="ui active inverted dimmer"> + <div class="ui loader"></div> + </div> + <action-table + v-if="result" + @action-launched="fetchData" + :objects-data="result" + :actions="actions" + :filters="actionFilters"> + <template slot="header-cells"> + <th><translate>Name</translate></th> + <th><translate>First seen</translate></th> + <th><translate>Users</translate></th> + <th><translate>Last activity</translate></th> + <th><translate>Received messages</translate></th> + </template> + <template slot="row-cells" slot-scope="scope"> + <td> + <router-link :to="{name: 'manage.moderation.domain.detail', params: {id: scope.obj.name }}">{{ scope.obj.name }}</router-link> + </td> + <td> + <human-date :date="scope.obj.creation_date"></human-date> + </td> + <td> + {{ scope.obj.actors_count }} + </td> + <td> + <human-date v-if="scope.obj.last_activity_date" :date="scope.obj.last_activity_date"></human-date> + <translate v-else>N/A</translate> + </td> + <td> + {{ scope.obj.outbox_activities_count }} + </td> + </template> + </action-table> + </div> + <div> + <pagination + v-if="result && result.count > paginateBy" + @page-changed="selectPage" + :compact="true" + :current="page" + :paginate-by="paginateBy" + :total="result.count" + ></pagination> + + <span v-if="result && result.results.length > 0"> + <translate + :translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}"> + Showing results %{ start }-%{ end } on %{ total } + </translate> + </span> + </div> + </div> +</template> + +<script> +import axios from 'axios' +import _ from '@/lodash' +import time from '@/utils/time' +import Pagination from '@/components/Pagination' +import ActionTable from '@/components/common/ActionTable' +import OrderingMixin from '@/components/mixins/Ordering' +import TranslationsMixin from '@/components/mixins/Translations' + +export default { + mixins: [OrderingMixin, TranslationsMixin], + props: { + filters: {type: Object, required: false} + }, + components: { + Pagination, + ActionTable + }, + data () { + let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + return { + time, + isLoading: false, + result: null, + page: 1, + paginateBy: 50, + search: '', + orderingDirection: defaultOrdering.direction || '+', + ordering: defaultOrdering.field, + orderingOptions: [ + ['name', 'name'], + ['creation_date', 'first_seen'], + ['last_activity_date', 'last_activity'], + ['actors_count', 'users'], + ['outbox_activities_count', 'received_messages'] + ] + + } + }, + created () { + this.fetchData() + }, + methods: { + fetchData () { + let params = _.merge({ + 'page': this.page, + 'page_size': this.paginateBy, + 'q': this.search, + 'ordering': this.getOrderingAsString() + }, this.filters) + let self = this + self.isLoading = true + self.checked = [] + axios.get('/manage/federation/domains/', {params: params}).then((response) => { + self.result = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + selectPage: function (page) { + this.page = page + } + }, + computed: { + labels () { + return { + searchPlaceholder: this.$gettext('Search by name...') + } + }, + actionFilters () { + var currentFilters = { + q: this.search + } + if (this.filters) { + return _.merge(currentFilters, this.filters) + } else { + return currentFilters + } + }, + actions () { + return [ + // { + // name: 'delete', + // label: this.$gettext('Delete'), + // isDangerous: true + // } + ] + } + }, + watch: { + search (newValue) { + this.page = 1 + this.fetchData() + }, + page () { + this.fetchData() + }, + ordering () { + this.fetchData() + }, + orderingDirection () { + this.fetchData() + } + } +} +</script> diff --git a/front/src/components/mixins/Translations.vue b/front/src/components/mixins/Translations.vue index be35c2f3432d419123a7432ac976cb7b21d49cc0..c982c9ad7ffedea54bae8664569166ad8e705c09 100644 --- a/front/src/components/mixins/Translations.vue +++ b/front/src/components/mixins/Translations.vue @@ -15,6 +15,7 @@ export default { }, filters: { creation_date: this.$gettext('Creation date'), + first_seen: this.$gettext('First seen date'), accessed_date: this.$gettext('Accessed date'), modification_date: this.$gettext('Modification date'), imported_date: this.$gettext('Imported date'), @@ -30,6 +31,8 @@ export default { date_joined: this.$gettext('Sign-up date'), last_activity: this.$gettext('Last activity'), username: this.$gettext('Username'), + users: this.$gettext('Users'), + received_messages: this.$gettext('Received messages'), } } } diff --git a/front/src/router/index.js b/front/src/router/index.js index f6b4d309f2f1ef1c8b60518587feb3149d9d6841..55bb4fc8b10400a7daa3079aa9c0008d947d4f2d 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -30,6 +30,8 @@ import AdminUsersBase from '@/views/admin/users/Base' import AdminUsersDetail from '@/views/admin/users/UsersDetail' import AdminUsersList from '@/views/admin/users/UsersList' import AdminInvitationsList from '@/views/admin/users/InvitationsList' +import AdminModerationBase from '@/views/admin/moderation/Base' +import AdminDomainsList from '@/views/admin/moderation/DomainsList' import ContentBase from '@/views/content/Base' import ContentHome from '@/views/content/Home' import LibrariesHome from '@/views/content/libraries/Home' @@ -224,6 +226,17 @@ export default new Router({ } ] }, + { + path: '/manage/moderation', + component: AdminModerationBase, + children: [ + { + path: 'domains', + name: 'manage.moderation.domains.list', + component: AdminDomainsList + } + ] + }, { path: '/library', component: Library, diff --git a/front/src/store/auth.js b/front/src/store/auth.js index 70dbe26babe36933a111f286dc257a8236bb2dea..1299dabfe879a76651093384c3e2b55d178e2475 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -9,10 +9,9 @@ export default { authenticated: false, username: '', availablePermissions: { - federation: false, settings: false, library: false, - upload: false + moderation: false }, profile: null, token: '', diff --git a/front/src/views/admin/moderation/Base.vue b/front/src/views/admin/moderation/Base.vue new file mode 100644 index 0000000000000000000000000000000000000000..d4487339d3f41bb665e67c7034fdf974f7a72095 --- /dev/null +++ b/front/src/views/admin/moderation/Base.vue @@ -0,0 +1,23 @@ +<template> + <div class="main pusher" v-title="labels.manageDomains"> + <nav class="ui secondary pointing menu" role="navigation" :aria-label="labels.secondaryMenu"> + <router-link + class="ui item" + :to="{name: 'manage.moderation.domains.list'}"><translate>Domains</translate></router-link> + </nav> + <router-view :key="$route.fullPath"></router-view> + </div> +</template> + +<script> +export default { + computed: { + labels() { + return { + manageDomains: this.$gettext("Manage domains"), + secondaryMenu: this.$gettext("Secondary menu") + } + } + } +} +</script> diff --git a/front/src/views/admin/moderation/DomainsList.vue b/front/src/views/admin/moderation/DomainsList.vue new file mode 100644 index 0000000000000000000000000000000000000000..84fb1df4300dffecc15db22048bafc50fb0e27dc --- /dev/null +++ b/front/src/views/admin/moderation/DomainsList.vue @@ -0,0 +1,30 @@ +<template> + <main v-title="labels.domains"> + <section class="ui vertical stripe segment"> + <h2 class="ui header"><translate>Domains</translate></h2> + <div class="ui hidden divider"></div> + <domains-table></domains-table> + </section> + </main> +</template> + +<script> +import DomainsTable from "@/components/manage/moderation/DomainsTable" + +export default { + components: { + DomainsTable + }, + computed: { + labels() { + return { + domains: this.$gettext("Domains") + } + } + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style>