mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #2526 from modoboa/feature/display_alarms_ui_v2
Added new page to display opened alarms in UI v2.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import repository from './repository'
|
||||
|
||||
const resource = 'alarms'
|
||||
|
||||
export default {
|
||||
getAll () {
|
||||
return repository.get(`${resource}/`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" small>
|
||||
Actions <v-icon right>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
:headers="headers"
|
||||
:items="alarms"
|
||||
:search="search"
|
||||
item-key="pk"
|
||||
class="elevation-1"
|
||||
show-select
|
||||
>
|
||||
<template v-slot:item.created="{ item }">
|
||||
{{ item.created|date }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alarms from '@/api/alarms'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
alarms: [],
|
||||
headers: [
|
||||
{ text: this.$gettext('Date'), value: 'created' },
|
||||
{ text: this.$gettext('Domain'), value: 'domain.name' },
|
||||
{ text: this.$gettext('Mailbox'), value: 'mailbox.name' },
|
||||
{ text: this.$gettext('Message'), value: 'title' }
|
||||
],
|
||||
search: '',
|
||||
selected: []
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
alarms.getAll().then(resp => {
|
||||
this.alarms = resp.data
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -75,11 +75,22 @@
|
||||
<div class="text-right">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-badge
|
||||
v-if="item.opened_alarms_count"
|
||||
bordered
|
||||
color="error"
|
||||
icon="mdi-bell"
|
||||
overlap
|
||||
>
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</v-badge>
|
||||
<v-btn v-else icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<menu-items :items="domainMenuItems" :object="item" />
|
||||
<menu-items :items="getDomainMenuItems(item)" :object="item" />
|
||||
</v-menu>
|
||||
</div>
|
||||
</td>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title><translate>Opened alarms</translate></v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<alarm-list />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AlarmList from '@/components/alarms/AlarmList'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AlarmList
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user