Domain detail page.

This commit is contained in:
Antoine Nguyen
2021-03-03 08:58:15 +01:00
parent da5114f55d
commit a9ed6b0fcb
12 changed files with 364 additions and 77 deletions
+16 -16
View File
@@ -14,23 +14,23 @@
</v-container>
</v-main>
<v-snackbar
v-model="snackbar"
:color="notificationColor"
:timeout="notificationTimeout"
top
>
{{ notification }}
v-model="snackbar"
:color="notificationColor"
:timeout="notificationTimeout"
top
>
{{ notification }}
<template v-slot:action="{ attrs }">
<v-btn
color="white"
text
v-bind="attrs"
@click="snackbar = false"
>
<translate>Close</translate>
</v-btn>
</template>
<template v-slot:action="{ attrs }">
<v-btn
color="white"
text
v-bind="attrs"
@click="snackbar = false"
>
<translate>Close</translate>
</v-btn>
</template>
</v-snackbar>
</v-app>
</template>
@@ -0,0 +1,45 @@
<template>
<v-chip :color="chipType"
@click="$emit('click', $event)"
small>
{{ label }}
</v-chip>
</template>
<script>
export default {
props: ['status'],
computed: {
label () {
if (this.status === 'disabled') {
return this.$gettext('Disabled')
}
if (this.status === 'pending') {
return this.$gettext('Pending')
}
if (this.status === 'critical') {
return this.$gettext('Problem')
}
if (this.status === 'ok') {
return this.$gettext('Valid')
}
return this.$gettext('Unknown')
},
chipType () {
if (this.status === 'disabled') {
return ''
}
if (this.status === 'pending') {
return 'info'
}
if (this.status === 'critical') {
return 'error'
}
if (this.status === 'ok') {
return 'success'
}
return 'warning'
}
}
}
</script>
@@ -0,0 +1,76 @@
<template>
<v-card>
<v-card-title>
<span class="headline">
<translate>DNS configuration for </translate> {{ domain.name }}
</span>
</v-card-title>
<v-card-text>
<p>Here is an example of DNS configuration (bind format). Replace values between [ ] by the appropriate ones.</p>
<v-alert
border="left"
colored-border
color="primary lighten-3"
elevation="2"
dense
>
<div class="title">MX</div>
<pre>
mail.{{ domain.name }}. IN A <strong>[<translate>IP address of your Modoboa server</translate>]</strong>
@ IN MX 10 mail.{{ domain.name }}.</pre>
</v-alert>
<v-alert
border="left"
colored-border
color="primary lighten-3"
elevation="2"
dense
>
<div class="title">SPF</div>
<pre>
@ IN TXT "v=spf1 mx ~all"</pre>
</v-alert>
<v-alert
border="left"
colored-border
color="primary lighten-3"
elevation="2"
dense
>
<div class="title">DMARC</div>
<pre>
_dmarc.{{ domain.name }}. IN TXT "v=DMARC1; p=quarantine; pct=100"</pre>
</v-alert>
<v-alert
border="left"
colored-border
color="primary lighten-3"
elevation="2"
dense
>
<div class="title">Auto configuration</div>
<pre>
autoconfig.{{ domain.name }}. IN CNAME <strong>[<translate>hostname of your automx server</translate>]</strong>
autodiscover.{{ domain.name }}. IN CNAME <strong>[<translate>hostname of your automx server</translate>]</strong></pre>
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey darken-1" text @click="close">
<translate>Close</translate>
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: ['domain'],
methods: {
close () {
this.$emit('close')
}
}
}
</script>
@@ -0,0 +1,64 @@
<template>
<v-card>
<v-card-title>
<span class="headline">DNS</span>
<v-btn icon :title="'DNS configuration help'|translate" @click="showConfigHelp = true">
<v-icon>mdi-information-outline</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6"><translate>Status</translate></v-col>
<v-col cols="6">
<dns-status-chip :status="domain.dns_global_status" @click="showDetail = true" />
</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>DKIM key</translate></v-col>
<v-col cols="6">
<div v-if="domain.dkim_public_key">
<v-btn color="primary" small>
<translate>Show key</translate>
</v-btn>
<v-btn icon><v-icon>mdi-refresh</v-icon></v-btn>
</div>
<translate v-else>Not generated</translate>
</v-col>
</v-row>
</v-card-text>
<v-dialog v-model="showConfigHelp"
max-width="800px"
persistent
>
<domain-dns-config :domain="domain" @close="showConfigHelp = false" />
</v-dialog>
<v-dialog v-model="showDetail"
persistent
max-width="800px">
<dns-detail @close="showDetail = false" :domain="domain" />
</v-dialog>
</v-card>
</template>
<script>
import DomainDNSConfig from './DomainDNSConfig'
import DNSDetail from './DNSDetail'
import DNSStatusChip from './DNSStatusChip'
export default {
components: {
'dns-detail': DNSDetail,
'dns-status-chip': DNSStatusChip,
'domain-dns-config': DomainDNSConfig
},
props: ['domain'],
data () {
return {
showConfigHelp: false,
showDetail: false
}
}
}
</script>
+24 -44
View File
@@ -39,9 +39,10 @@
<v-checkbox />
</td>
<td>
<router-link :to="{ name: 'DomainDetail', params: { pk: item.pk } }">
<router-link :to="{ name: 'DomainDetail', params: { id: item.pk } }">
{{ item.name }}
</router-link>
<translate v-if="!item.enabled" class="ml-2 grey--text">(disabled)</translate>
</td>
<td>
{{ item.domainalias_count }} aliases
@@ -54,11 +55,7 @@
</v-btn>
</td>
<td>
<v-chip :color="getDNSTagType(item.dns_global_status)"
@click="openDNSDetail(item)"
small>
{{ getDNSLabel(item.dns_global_status) }}
</v-chip>
<dns-status-chip :status="item.dns_global_status" @click="openDNSDetail(item)" />
</td>
<td>
<v-progress-linear v-model="item.allocated_quota_in_percent" />
@@ -94,7 +91,12 @@
</tr>
</template>
</v-data-table>
<confirm-dialog ref="confirm" />
<confirm-dialog ref="confirm">
<v-checkbox v-model="keepDomainFolder"
:label="'Do not delete domain folder'|translate"
hide-details
/>
</confirm-dialog>
<v-dialog v-model="showDNSdetail"
persistent
max-width="800px">
@@ -134,6 +136,7 @@ import domainApi from '@/api/domains'
import AdministratorList from './AdministratorList'
import ConfirmDialog from '@/components/layout/ConfirmDialog'
import DNSDetail from '@/components/domains/DNSDetail'
import DNSStatusChip from './DNSStatusChip'
import DomainAliasForm from '@/components/domains/DomainAliasForm'
import DomainForm from '@/components/domains/DomainForm'
import MenuItems from '@/components/tools/MenuItems'
@@ -143,6 +146,7 @@ export default {
AdministratorList,
ConfirmDialog,
'dns-detail': DNSDetail,
'dns-status-chip': DNSStatusChip,
DomainAliasForm,
DomainForm,
MenuItems
@@ -158,13 +162,14 @@ export default {
{ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteDomain, color: 'red' }
],
headers: [
{ text: 'Name', value: 'name' },
{ text: 'Aliases', value: 'domainalias_count' },
{ text: 'DNS status', value: 'dns_global_status', sortable: false, align: 'center' },
{ text: 'Sending limit', value: 'message_limit' },
{ text: 'Quota', value: 'allocated_quota_in_percent' },
{ text: this.$gettext('Name'), value: 'name' },
{ text: this.$gettext('Aliases'), value: 'domainalias_count' },
{ text: this.$gettext('DNS status'), value: 'dns_global_status', sortable: false, align: 'center' },
{ text: this.$gettext('Sending limit'), value: 'message_limit' },
{ text: this.$gettext('Quota'), value: 'allocated_quota_in_percent' },
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
],
keepDomainFolder: false,
selectedDomain: null,
selectedDomainAlias: null,
showAdminList: false,
@@ -200,8 +205,10 @@ export default {
if (!confirm) {
return
}
this.$store.dispatch('domains/deleteDomain', { id: domain.pk }).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Domain delete') })
const data = { keep_folder: this.keepDomainFolder }
this.$store.dispatch('domains/deleteDomain', { id: domain.pk, data }).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Domain deleted') })
this.keepDomainFolder = false
})
},
domainAliasDeleted () {
@@ -220,36 +227,6 @@ export default {
this.selectedDomainAlias = alias
this.showAliasForm = true
},
getDNSLabel (value) {
if (value === 'disabled') {
return this.$gettext('Disabled')
}
if (value === 'pending') {
return this.$gettext('Pending')
}
if (value === 'critical') {
return this.$gettext('Problem')
}
if (value === 'ok') {
return this.$gettext('Valid')
}
return this.$gettext('Unknown')
},
getDNSTagType (value) {
if (value === 'disabled') {
return ''
}
if (value === 'pending') {
return 'info'
}
if (value === 'critical') {
return 'error'
}
if (value === 'ok') {
return 'success'
}
return 'warning'
},
loadAliases ({ item, value }) {
if (!value) {
return
@@ -277,4 +254,7 @@ export default {
.v-text-field--outlined >>> fieldset {
border-color: #BFC5D2;
}
.v-input--checkbox >>> .v-label {
font-size: 0.875rem !important;
}
</style>
@@ -0,0 +1,40 @@
<template>
<v-card>
<v-card-text>
<v-row>
<v-col cols="6"><translate>Creation date</translate></v-col>
<v-col cols="6">{{ domain.creation|date }}</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Last modification date</translate></v-col>
<v-col cols="6">{{ domain.last_modification|date }}</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Quota</translate></v-col>
<v-col cols="6">{{ domain.quota }} MB</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Default mailbox quota</translate></v-col>
<v-col cols="6">{{ domain.default_mailbox_quota }} MB</v-col>
</v-row>
<v-row v-if="domain.sending_limit !== undefined">
<v-col cols="6"><translate>Daily sending limit</translate></v-col>
<v-col cols="6">{{ domain.sending_limit }} <translate>messages</translate></v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Mailboxes</translate></v-col>
<v-col cols="6">{{ domain.mailbox_count }}</v-col>
</v-row>
<v-row>
<v-col cols="6"><translate>Aliases</translate></v-col>
<v-col cols="6">{{ domain.mbalias_count }}</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: ['domain']
}
</script>
@@ -14,18 +14,20 @@
<v-card-text
v-show="!!message"
class="pa-4"
v-html="message"
></v-card-text>
>
{{ message }}
<slot></slot>
</v-card-text>
<v-card-actions class="pt-3">
<v-spacer></v-spacer>
<v-btn
v-if="!options.noconfirm"
:color="this.options.color"
@click.native="cancel"
>
{{ options.cancelLabel }}
</v-btn>
<v-btn
:color="this.options.color"
@click.native="agree">
{{ options.agreeLabel }}
</v-btn>
@@ -15,24 +15,32 @@
<v-card>
<v-card-text>
<template v-for="(param, index) in displayableParams(element.parameters)">
<v-checkbox :key="index"
:label="param.label"
v-model="parameters[param.name]"
v-if="param.widget === 'BooleanField'"
/>
<v-switch :key="index"
:label="param.label"
v-model="parameters[param.name]"
:hint="param.help_text"
persistent-hint
class="my-2"
v-if="param.widget === 'BooleanField'"
/>
<v-select :key="index"
:label="param.label"
v-model="parameters[param.name]"
:items="param.choices"
:hint="param.help_text"
persistent-hint
class="my-2"
v-else-if="param.widget === 'ChoiceField'"
/>
<v-text-field
:key="index"
:label="param.label"
:hint="param.help_text"
persistent-hint
v-model="parameters[param.name]"
:error="formErrors[param.name] !== undefined"
:error-messages="formErrors[param.name]"
class="my-2"
v-else
/>
</template>
+8
View File
@@ -25,6 +25,14 @@ const routes = [
requiresAuth: true
}
},
{
path: '/domains/:id',
name: 'DomainDetail',
component: () => import('../views/Domain.vue'),
meta: {
requiresAuth: true
}
},
{
path: '/parameters/:app',
name: 'ParametersEdit',
+70
View File
@@ -0,0 +1,70 @@
<template>
<div>
<v-toolbar flat>
<v-toolbar-title><translate>Domain</translate> {{ domain.name }}</v-toolbar-title>
<v-btn color="primary" icon @click="showEditForm = true"><v-icon>mdi-circle-edit-outline</v-icon></v-btn>
<v-btn icon @click="showAdminList = true" :title="'Show administrator list'|translate">
<v-icon>mdi-account-supervisor</v-icon>
</v-btn>
</v-toolbar>
<v-layout>
<v-row>
<v-col cols="6">
<domain-summary :domain="domain" />
<div class="mt-4" />
<domain-dns-summary :domain="domain" />
</v-col>
</v-row>
</v-layout>
<v-dialog v-model="showEditForm"
persistent
max-width="800px"
>
<domain-form :domain="domain" @close="showEditForm = false" />
</v-dialog>
<v-dialog v-model="showAdminList"
persistent
max-width="800px"
>
<administrator-list
:domain="domain"
@close="showAdminList = false"
/>
</v-dialog>
</div>
</template>
<script>
import domains from '@/api/domains'
import AdministratorList from '@/components/domains/AdministratorList'
import DomainForm from '@/components/domains/DomainForm'
import DomainDNSSummary from '@/components/domains/DomainDNSSummary'
import DomainSummary from '@/components/domains/DomainSummary'
export default {
components: {
AdministratorList,
DomainForm,
'domain-dns-summary': DomainDNSSummary,
DomainSummary
},
data () {
return {
domain: {},
showEditForm: false,
showAdminList: false
}
},
mounted () {
domains.getDomain(this.$route.params.id).then(resp => {
this.domain = resp.data
})
}
}
</script>
<style scoped>
.v-toolbar {
background-color: #f7f8fa !important;
}
</style>
+3 -2
View File
@@ -52,12 +52,13 @@ class DomainSerializer(serializers.ModelSerializer):
"enable_dkim", "dkim_key_selector", "dkim_key_length",
"dkim_public_key", "dkim_private_key_path",
"mailbox_count", "mbalias_count", "domainalias_count",
"dns_global_status", "message_limit"
"dns_global_status", "message_limit", "creation",
"last_modification"
)
read_only_fields = (
"pk", "dkim_public_key", "dns_global_status", "allocated_quota_in_percent"
"mailbox_count", "mbalias_count", "domainalias_count",
"enable_dns_checks"
"enable_dns_checks", "creation", "last_modification"
)
def validate_name(self, value):
-7
View File
@@ -334,13 +334,6 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
" renamed or removed within Modoboa"
)
}),
("mailboxes_owner", {
"label": ugettext_lazy("Mailboxes owner"),
"display": "handle_mailboxes=true",
"help_text": ugettext_lazy(
"The UNIX account who owns mailboxes on the filesystem"
)
}),
("default_domain_quota", {
"label": ugettext_lazy("Default domain quota"),
"help_text": ugettext_lazy(