diff --git a/frontend/src/api/alarms.js b/frontend/src/api/alarms.js new file mode 100644 index 000000000..62ac76676 --- /dev/null +++ b/frontend/src/api/alarms.js @@ -0,0 +1,9 @@ +import repository from './repository' + +const resource = 'alarms' + +export default { + getAll () { + return repository.get(`${resource}/`) + } +} diff --git a/frontend/src/components/alarms/AlarmList.vue b/frontend/src/components/alarms/AlarmList.vue new file mode 100644 index 000000000..842c17147 --- /dev/null +++ b/frontend/src/components/alarms/AlarmList.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/components/domains/DomainList.vue b/frontend/src/components/domains/DomainList.vue index be5958cbd..1a2b7db4d 100644 --- a/frontend/src/components/domains/DomainList.vue +++ b/frontend/src/components/domains/DomainList.vue @@ -75,11 +75,22 @@
- +
@@ -151,11 +162,6 @@ export default { }), data () { return { - domainMenuItems: [ - { label: this.$gettext('Administrators'), icon: 'mdi-account-supervisor', onClick: this.openAdminList }, - { label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editDomain }, - { label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteDomain, color: 'red' } - ], headers: [ { text: this.$gettext('Name'), value: 'name' }, { text: this.$gettext('Aliases'), value: 'domainalias_count' }, @@ -243,6 +249,22 @@ export default { }, showAliases (item) { this.expanded = [{ item }] + }, + getDomainMenuItems (domain) { + const result = [ + { label: this.$gettext('Administrators'), icon: 'mdi-account-supervisor', onClick: this.openAdminList }, + { label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editDomain }, + { label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteDomain, color: 'red' } + ] + if (domain.opened_alarms_count) { + result.push({ + label: this.$gettext('Alarms'), + icon: 'mdi-bell', + color: 'red', + onClick: () => this.$router.push({ name: 'Alarms' }) + }) + } + return result } } } diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index cff2f31d4..a0d16369d 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -174,6 +174,11 @@ export default { icon: 'mdi-account', roles: ['DomainAdmins', 'Resellers', 'SuperAdmins'] }, + { + text: this.$gettext('Alarms'), + to: { name: 'Alarms' }, + icon: 'mdi-bell' + }, { icon: 'mdi-history', text: this.$gettext('Logs'), diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 70917c6b7..42bd0cc0f 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -110,6 +110,14 @@ const routes = [ allowedRoles: ['SuperAdmins'] } }, + { + path: '/alarms', + name: 'Alarms', + component: () => import('../views/alarms/Alarms.vue'), + meta: { + requiresAuth: true + } + }, { path: '/logs/audit_trail', name: 'AuditTrail', diff --git a/frontend/src/views/alarms/Alarms.vue b/frontend/src/views/alarms/Alarms.vue new file mode 100644 index 000000000..2268169e4 --- /dev/null +++ b/frontend/src/views/alarms/Alarms.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py index 62fe5fb51..84bccec49 100644 --- a/modoboa/admin/api/v2/serializers.py +++ b/modoboa/admin/api/v2/serializers.py @@ -2,6 +2,7 @@ import ipaddress import os +from typing import List from django.conf import settings from django.core.exceptions import ValidationError @@ -54,7 +55,7 @@ class DomainSerializer(v1_serializers.DomainSerializer): class Meta(v1_serializers.DomainSerializer.Meta): fields = v1_serializers.DomainSerializer.Meta.fields + ( - "domain_admin", "transport" + "domain_admin", "transport", "opened_alarms_count" ) def __init__(self, *args, **kwargs): @@ -321,7 +322,7 @@ class AccountSerializer(v1_serializers.AccountSerializer): return self.fields["resources"] = serializers.SerializerMethodField() - def get_resources(self, account): + def get_resources(self, account) -> List[AccountResourceSerializer]: if account.role == 'SimpleUsers': return [] resources = [] @@ -578,3 +579,12 @@ class CSVIdentityImportSerializer(CSVImportSerializer): """Custom serializer for identity import.""" crypt_password = serializers.BooleanField() + + +class AlarmSerializer(serializers.ModelSerializer): + """Serializer for Alarm related endpoints.""" + + class Meta: + depth = 1 + fields = "__all__" + model = models.Alarm diff --git a/modoboa/admin/api/v2/tests.py b/modoboa/admin/api/v2/tests.py index bcbd146b7..d1cec2bd2 100644 --- a/modoboa/admin/api/v2/tests.py +++ b/modoboa/admin/api/v2/tests.py @@ -467,3 +467,20 @@ class UserAccountViewSetTestCase(ModoAPITestCase): models.Alias.objects.filter(address=self.da.username).count(), 1 ) + + +class AlarmViewSetTestCase(ModoAPITestCase): + + @classmethod + def setUpTestData(cls): # NOQA:N802 + """Create test data.""" + super().setUpTestData() + factories.populate_database() + factories.AlarmFactory( + domain__name="test.com", mailbox=None, title="Test alarm") + + def test_list(self): + url = reverse("v2:alarm-list") + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()), 1) diff --git a/modoboa/admin/api/v2/urls.py b/modoboa/admin/api/v2/urls.py index d49f66bbc..aa0e04b9d 100644 --- a/modoboa/admin/api/v2/urls.py +++ b/modoboa/admin/api/v2/urls.py @@ -19,5 +19,7 @@ router.register( r"identities", viewsets.IdentityViewSet, basename="identities") router.register( r"account", viewsets.UserAccountViewSet, basename="account") +router.register( + r"alarms", viewsets.AlarmViewSet, basename="alarm") urlpatterns = router.urls diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py index f4908a643..1c4400488 100644 --- a/modoboa/admin/api/v2/viewsets.py +++ b/modoboa/admin/api/v2/viewsets.py @@ -321,3 +321,18 @@ class UserAccountViewSet(viewsets.ViewSet): ).update(enabled=False) alias.set_recipients(recipients) return response.Response(serializer.validated_data) + + +class AlarmViewSet(viewsets.ReadOnlyModelViewSet): + """Viewset for Alarm.""" + + permission_classes = ( + permissions.IsAuthenticated, + ) + serializer_class = serializers.AlarmSerializer + + def get_queryset(self): + return models.Alarm.objects.filter( + domain__in=models.Domain.objects.get_for_admin( + self.request.user) + ) diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index ea7b6f02f..66bb412a5 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -92,12 +92,17 @@ class Domain(mixins.MessageLimitMixin, AdminObject): return self.alias_set.filter(internal=False).count() @property - def identities_count(self): + def identities_count(self) -> int: """Total number of identities in this domain.""" return ( self.mailbox_set.count() + self.alias_set.filter(internal=False).count()) + @property + def opened_alarms_count(self) -> int: + """Number of alarms currently opened for this domain.""" + return self.alarms.opened().count() + @property def tags(self): label = "" diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index 9c87f8494..65445c342 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -13,7 +13,7 @@ class ObjectLimitMixin(object): """Common methods.""" @property - def label(self): + def label(self) -> str: """Return display name.""" return self.definition["label"] @@ -23,7 +23,7 @@ class ObjectLimitMixin(object): return self.definition.get("type", "count") @property - def usage(self): + def usage(self) -> int: """Return current limit usage in %.""" if self.max_value < 0: return -1 @@ -79,7 +79,7 @@ class UserObjectLimit(ObjectLimitMixin, models.Model): return None @property - def current_value(self): + def current_value(self) -> int: """Return the current number of objects.""" condition = ( self.type == "count" and @@ -107,7 +107,7 @@ class UserObjectLimit(ObjectLimitMixin, models.Model): return self.definition["label"] @cached_property - def usage(self): + def usage(self) -> int: """Return current limit usage in %.""" if self.max_value < 0: return -1 @@ -141,7 +141,7 @@ class DomainObjectLimit(ObjectLimitMixin, models.Model): return None @property - def current_value(self): + def current_value(self) -> int: """Return the current number of objects.""" definition = self.definition if not definition: