UI/UX and improvements and bug fixes:

- port validation added
- NPE fixes
- removed hard coded wait in integration test
- DNSRecordsTab: client side pagination, column sorting and search
- UX improvement for CreateDnsRecord, DeleteDnsZone and DeleteDnsServer
- All DNS forms: wire @finish and htmltype
This commit is contained in:
Manoj Kumar 2026-06-05 11:28:23 +05:30
parent 2aae36d3e1
commit e7b7b4cbbb
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
16 changed files with 367 additions and 201 deletions

View File

@ -43,7 +43,7 @@ import com.cloud.utils.EnumUtils;
description = "Adds a new external DNS server",
responseObject = DnsServerResponse.class,
entityType = {DnsServer.class},
requestHasSensitiveInfo = false,
requestHasSensitiveInfo = true,
responseHasSensitiveInfo = false,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})

View File

@ -85,7 +85,7 @@ public class CreateDnsRecordCmd extends BaseAsyncCmd {
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (Exception e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create DNS Record: " + e.getMessage());
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
}
}

View File

@ -30,7 +30,6 @@ import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.DnsServerResponse;
import org.apache.cloudstack.dns.DnsServer;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.user.Account;
@ -40,7 +39,7 @@ import com.cloud.utils.EnumUtils;
description = "Update DNS server",
responseObject = DnsServerResponse.class,
entityType = {DnsServer.class},
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
requestHasSensitiveInfo = true, responseHasSensitiveInfo = false,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class UpdateDnsServerCmd extends BaseCmd {
@ -96,7 +95,7 @@ public class UpdateDnsServerCmd extends BaseCmd {
return port;
}
public Boolean isPublic() {
return BooleanUtils.isTrue(isPublic);
return isPublic;
}
public String getPublicDomainSuffix() {
return publicDomainSuffix;

View File

@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@ -77,6 +78,7 @@ import org.apache.cloudstack.framework.messagebus.MessageBus;
import org.apache.cloudstack.framework.messagebus.MessageSubscriber;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.stereotype.Component;
import com.cloud.domain.Domain;
@ -172,8 +174,8 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
boolean isDnsPublic = cmd.isPublic();
String publicDomainSuffix = cmd.getPublicDomainSuffix();
if (caller.getType().equals(Account.Type.NORMAL)) {
logger.info("Only admin and domain admin users are allowed to configure a public DNS server");
if (!accountMgr.isRootAdmin(caller.getId()) && !accountMgr.isDomainAdmin(caller.getId())) {
logger.info("Only root admin and domain admin users are allowed to configure a public DNS server");
isDnsPublic = false;
publicDomainSuffix = null;
}
@ -264,15 +266,21 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
validationRequired = true;
}
if (cmd.getPort() != null) {
if (cmd.getPort() != null && !Objects.equals(cmd.getPort(), dnsServer.getPort())) {
dnsServer.setPort(cmd.getPort());
validationRequired = true;
}
if (cmd.isPublic() != null) {
dnsServer.setPublicServer(cmd.isPublic());
}
if (accountMgr.isRootAdmin(caller.getId()) || accountMgr.isDomainAdmin(caller.getId())) {
if (cmd.isPublic() != null) {
boolean isPublic = BooleanUtils.isTrue(cmd.isPublic());
dnsServer.setPublicServer(isPublic);
if (cmd.getPublicDomainSuffix() != null) {
dnsServer.setPublicDomainSuffix(DnsProviderUtil.normalizeDomainForDb(cmd.getPublicDomainSuffix()));
String publicDomainSuffix = null;
if (isPublic && StringUtils.isNotBlank(cmd.getPublicDomainSuffix())) {
publicDomainSuffix = DnsProviderUtil.normalizeDomainForDb(cmd.getPublicDomainSuffix());
}
dnsServer.setPublicDomainSuffix(publicDomainSuffix);
}
}
if (cmd.getNameServers() != null) {
@ -440,6 +448,9 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
Account caller = CallContext.current().getCallingAccount();
if (cmd.getDnsServerId() != null) {
DnsServer dnsServer = dnsServerDao.findById(cmd.getDnsServerId());
if (dnsServer == null) {
throw new InvalidParameterValueException("DNS server not found for the provided ID");
}
accountMgr.checkAccess(caller, null, true, dnsServer);
}
List<Long> ownDnsServerIds = dnsServerDao.listDnsServerIdsByAccountId(caller.getAccountId());
@ -501,6 +512,9 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
Account caller = CallContext.current().getCallingAccount();
accountMgr.checkAccess(caller, null, true, dnsZone);
DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
if (server == null) {
throw new CloudRuntimeException("The underlying DNS server for this DNS Record is missing.");
}
DnsRecord.RecordType recordType = cmd.getType();
try {
DnsRecord record = new DnsRecord();
@ -593,6 +607,10 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
throw new CloudRuntimeException("DNS zone not found during provisioning");
}
DnsServerVO server = dnsServerDao.findById(dnsZone.getDnsServerId());
if (server == null) {
dnsZoneDao.remove(dnsZoneId);
throw new CloudRuntimeException(String.format("DNS server for zone '%s' no longer exists", dnsZone.getName()));
}
try {
DnsProvider provider = getProviderByType(server.getProviderType());
if (isExistingZone) {
@ -626,6 +644,9 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
public DnsServerResponse createDnsServerResponse(DnsServer dnsServer) {
DnsServerJoinVO serverJoin = dnsServerJoinDao.findById(dnsServer.getId());
if (serverJoin == null) {
throw new CloudRuntimeException(String.format("DNS server not found for ID: %s", dnsServer.getId()));
}
return createDnsServerResponse(serverJoin);
}
@ -650,6 +671,9 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
@Override
public DnsZoneResponse createDnsZoneResponse(DnsZone dnsZone) {
DnsZoneJoinVO zoneJoinVO = dnsZoneJoinDao.findById(dnsZone.getId());
if (zoneJoinVO == null) {
throw new CloudRuntimeException(String.format("DNS zone join view not found for ID: %s", dnsZone.getId()));
}
return createDnsZoneResponse(zoneJoinVO);
}

View File

@ -717,7 +717,7 @@ public class DnsProviderManagerImplTest {
public void testAddDnsServerSuccess() throws Exception {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(callerMock.getType()).thenReturn(Account.Type.ADMIN);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(true);
when(cmd.getUrl()).thenReturn("http://newpdns:8081");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(null);
@ -764,6 +764,7 @@ public class DnsProviderManagerImplTest {
org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.ListDnsZonesCmd.class);
when(cmd.getId()).thenReturn(null);
when(cmd.getDnsServerId()).thenReturn(null);
when(dnsServerDao.listDnsServerIdsByAccountId(anyLong())).thenReturn(Collections.emptyList());
List<DnsZoneVO> zones = Collections.singletonList(zoneVO);
com.cloud.utils.Pair<List<DnsZoneVO>, Integer> searchPair = new com.cloud.utils.Pair<>(zones, 1);
@ -789,7 +790,8 @@ public class DnsProviderManagerImplTest {
public void testAddDnsServerNormalUser() throws Exception {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(callerMock.getType()).thenReturn(Account.Type.NORMAL);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(false);
when(accountMgr.isDomainAdmin(callerMock.getId())).thenReturn(false);
when(cmd.getUrl()).thenReturn("http://newpdns:8081");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(cmd.getNameServers()).thenReturn(Collections.emptyList());
@ -808,7 +810,7 @@ public class DnsProviderManagerImplTest {
public void testAddDnsServerValidationFailure() throws Exception {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(callerMock.getType()).thenReturn(Account.Type.ADMIN);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(true);
when(cmd.getUrl()).thenReturn("http://newpdns:8081");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(cmd.getNameServers()).thenReturn(Collections.emptyList());

View File

@ -34,6 +34,7 @@ class TestCloudStackDNSFramework(cloudstackTestCase):
"""
super(TestCloudStackDNSFramework, cls).setUpClass()
cls.api_client = cls.testClient.getApiClient()
cls.pdns_unavailable = False
cls.logger = logging.getLogger("TestCloudStackDNSFramework")
cls.stream_handler = logging.StreamHandler()
@ -77,14 +78,41 @@ class TestCloudStackDNSFramework(cloudstackTestCase):
cls.tearDownClass()
raise Exception(f"Failed to start PDNS:\n{result.stderr}")
# Allow PDNS to initialize
time.sleep(15)
cls.logger.info("PDNS is up and running")
# Construct PDNS URL once
# Wait for PDNS to be ready with polling
cls.pdns_url = f"http://{cls.marvin_vm_ip}"
cls.logger.info(f"PDNS endpoint: {cls.pdns_url}")
cls.logger.info(f"PDNS endpoint: {cls.pdns_url}:8081")
cls._wait_for_pdns_ready()
@classmethod
def _wait_for_pdns_ready(cls, timeout=90, interval=1):
"""
Poll PDNS API until it responds or timeout is reached.
Logs a message and sets cls.pdns_unavailable if PDNS is not reachable.
"""
import urllib.request
api_url = f"{cls.pdns_url}:8081/api/v1/servers"
cls.logger.info(f"Waiting for PDNS to be ready at {api_url} (timeout: {timeout}s)")
for _ in range(timeout // interval):
try:
req = urllib.request.Request(api_url)
req.add_header("X-API-Key", "supersecretapikey")
resp = urllib.request.urlopen(req, timeout=2)
if resp.status == 200:
cls.logger.info("PDNS is up and running")
cls.pdns_unavailable = False
return
except Exception:
pass
time.sleep(interval)
cls.logger.warning("PDNS did not become ready within timeout; skipping tests")
cls.pdns_unavailable = True
def setUp(self):
if self.__class__.pdns_unavailable:
import unittest
raise unittest.SkipTest("PDNS is unavailable; skipping test")
@attr(tags=["advanced"], required_hardware="true")
def test_01_list_dns_providers(self):

View File

@ -958,6 +958,7 @@
"label.dns.create.zone": "Create DNS Zone",
"label.dns.delete.server": "Delete DNS Server",
"label.dns.delete.zone": "Delete DNS Zone",
"label.dns.delete.record": "Delete DNS Record",
"label.dns.dnsapikey": "DNS API key",
"label.dns.pdnsserverid": "PowerDNS server ID",
"label.dns.name": "DNS Name",
@ -3929,6 +3930,10 @@
"message.success.update.dns.server": "Successfully updated DNS server",
"message.success.update.dns.zone": "Successfully updated DNS zone",
"message.success.delete.dns.record": "Successfully deleted DNS record",
"message.success.delete.dns.server": "Successfully deleted DNS server",
"message.success.delete.dns.zone": "Successfully deleted DNS zone",
"message.error.delete.dns.server": "Failed to delete DNS server",
"message.error.delete.dns.zone": "Failed to delete DNS zone",
"message.success.associate.dns.zone": "Successfully associated DNS zone with network",
"message.error.fetch.dns.zones": "Could not load DNS zones.",
"message.success.add.guest.network": "Successfully created guest Network",

View File

@ -22,7 +22,8 @@
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
layout="vertical"
@finish="handleSubmit">
<a-form-item name="name" ref="name">
<template #label>
@ -102,16 +103,18 @@
:placeholder="'Enter PowerDNS Server ID (optional if localhost)'" />
</a-form-item>
<a-form-item v-if="isAdminOrDomainAdmin()" name="publicdomainsuffix" ref="publicdomainsuffix">
<template #label>
<tooltip-label
:title="$t('label.dns.publicdomainsuffix')"
:tooltip="apiParams.publicdomainsuffix?.description" />
</template>
<a-input
v-model:value="form.publicdomainsuffix"
:placeholder="apiParams.publicdomainsuffix?.description || 'Enter Public Domain Suffix e.g. example.com'" />
</a-form-item>
<Transition name="slide-down">
<a-form-item v-if="isAdminOrDomainAdmin() && form.ispublic" name="publicdomainsuffix" ref="publicdomainsuffix">
<template #label>
<tooltip-label
:title="$t('label.dns.publicdomainsuffix')"
:tooltip="apiParams.publicdomainsuffix?.description" />
</template>
<a-input
v-model:value="form.publicdomainsuffix"
:placeholder="apiParams.publicdomainsuffix?.description || 'Enter Public Domain Suffix e.g. example.com'" />
</a-form-item>
</Transition>
<a-form-item name="nameservers" ref="nameservers">
<template #label>
@ -143,7 +146,7 @@
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>
@ -234,11 +237,22 @@ export default {
if (this.form.pdnsserverid) {
params['details[0].pdnsServerId'] = this.form.pdnsserverid?.trim()
}
await postAPI('addDnsServer', params)
const response = await postAPI('addDnsServer', params)
const serverData = response?.adddnsserverresponse?.dnsserver || response?.adddnsserverresponse
if (!serverData?.id) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: 'Failed to get server from response',
duration: 0
})
this.loading = false
return
}
this.$notification.success({
message: this.$t('label.dns.add.server'),
description: this.$t('message.success.add.dns.server')
description: `${this.$t('message.success.add.dns.server')} ${params.name}`
})
this.loading = false
this.$emit('refresh-data')
this.closeAction()
} catch (error) {
@ -247,7 +261,6 @@ export default {
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},
@ -353,4 +366,16 @@ export default {
.action-button button {
margin-left: 8px;
}
.slide-down-enter-active,
.slide-down-leave-active {
transition: max-height 0.3s ease, opacity 0.3s ease;
overflow: hidden;
max-height: 120px;
}
.slide-down-enter-from,
.slide-down-leave-to {
max-height: 0;
opacity: 0;
}
</style>

View File

@ -22,7 +22,8 @@
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
layout="vertical"
@finish="handleSubmit">
<a-form-item name="dnszoneid" ref="dnszoneid">
<template #label>
@ -65,7 +66,7 @@
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>

View File

@ -22,7 +22,8 @@
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
layout="vertical"
@finish="handleSubmit">
<a-form-item name="name" ref="name">
<template #label>
@ -83,13 +84,13 @@
</a-form-item>
<div class="action-button">
<a-button @click="closeAction">
<a-button @click="$emit('close-action')">
{{ $t('label.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>
@ -165,36 +166,19 @@ export default {
description: 'Failed to get jobid for createDnsRecord',
duration: 0
})
this.loading = false
return
}
await this.$pollJob({
jobId: jobId,
title: this.$t('label.dns.create.record'),
description: this.$t('label.creating.dns.record'),
successMethod: () => {
this.$notification.success({
message: this.$t('label.dns.create.record'),
description: this.$t('message.success.create.dns.record')
})
},
loadingMessage: `${this.$t('label.dns.create.record')} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
action: { isFetchData: false }
})
this.$emit('refresh-data')
this.closeAction()
this.$emit('close-action', { jobId, recordName: params.name })
} catch (error) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},
closeAction () {
this.$emit('close-action')
},
async validateNumber (rule, value) {
if (value && (isNaN(value) || value <= 0)) {
return Promise.reject(this.$t('message.error.number'))

View File

@ -22,7 +22,8 @@
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
layout="vertical"
@finish="handleSubmit">
<a-form-item name="dnsserverid" ref="dnsserverid">
<template #label>
@ -81,7 +82,7 @@
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>
@ -154,22 +155,22 @@ export default {
description: 'Failed to get jobid for CreateDnsZone',
duration: 0
})
this.loading = false
return
}
await this.$pollJob({
this.$pollJob({
jobId: jobId,
title: this.$t('label.dns.create.zone'),
description: this.$t('label.creating.dns.zone'),
description: params.name,
successMethod: () => {
this.$notification.success({
message: this.$t('label.dns.create.zone'),
description: this.$t('message.success.create.dns.zone')
description: `${this.$t('message.success.create.dns.zone')} ${params.name}`
})
},
loadingMessage: `${this.$t('label.dns.create.zone')} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
action: { isFetchData: false }
loadingMessage: `${this.$t('label.dns.create.zone')} ${params.name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
})
this.$emit('refresh-data')
this.closeAction()
} catch (error) {
this.$notification.error({
@ -177,7 +178,6 @@ export default {
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},

View File

@ -50,7 +50,8 @@
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
layout="vertical"
@finish="handleSubmit">
<a-form-item name="name" ref="name">
<a-input
@ -82,7 +83,7 @@
danger
:disabled="form.name !== resource.name"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.delete') }}
</a-button>
</div>
@ -162,36 +163,50 @@ export default {
const response = await postAPI('deleteDnsServer', params)
const jobId = response?.deletednsserverresponse?.jobid
if (jobId) {
this.$pollJob({
jobId: jobId,
title: this.$t('label.dns.delete.server'),
description: this.resource.name,
successMethod: () => {
this.$notification.success({
message: this.$t('label.dns.delete.server'),
description: `Successfully deleted DNS server ${this.resource.name}`
})
},
loadingMessage: `${this.$t('label.dns.delete.server')} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
if (!jobId) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: 'Failed to get jobid for DeleteDnsServer',
duration: 0
})
this.loading = false
return
}
this.$pollJob({
jobId: jobId,
title: this.$t('label.dns.delete.server'),
description: this.resource.name,
successMethod: () => {
this.loading = false
this.$notification.success({
message: this.$t('label.dns.delete.server'),
description: `${this.$t('message.success.delete.dns.server')} ${this.resource.name}`
})
if (this.$route.path !== '/dnsserver') {
this.$router.push({ path: '/dnsserver' })
} else {
this.$emit('refresh-data')
}
if (this.$route.path !== '/dnsserver') {
this.$router.push({ path: '/dnsserver' })
} else {
this.$emit('refresh-data')
}
},
errorMethod: () => {
this.loading = false
this.$notification.error({
message: this.$t('label.dns.delete.server'),
description: this.$t('message.error.delete.dns.server')
})
},
loadingMessage: `${this.$t('label.dns.delete.server')} ${this.resource.name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
})
this.closeAction()
} catch (error) {
this.loading = false
this.$notification.error({
message: this.$t('message.request.failed'),
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},
closeAction () {

View File

@ -34,7 +34,8 @@
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
layout="vertical"
@finish="handleSubmit">
<a-form-item name="name" ref="name">
<a-input
@ -59,7 +60,7 @@
danger
:disabled="form.name !== resource.name"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.delete') }}
</a-button>
</div>
@ -113,36 +114,49 @@ export default {
const response = await postAPI('deleteDnsZone', params)
const jobId = response?.deletednszoneresponse?.jobid
const isDetailView = this.$route.path !== '/dnszone'
if (jobId) {
this.$pollJob({
jobId: jobId,
title: this.$t('label.dns.delete.zone'),
description: this.resource.name,
successMethod: () => {
this.$notification.success({
message: this.$t('label.dns.delete.zone'),
description: `Successfully deleted DNS zone ${this.resource.name}`
})
},
loadingMessage: `${this.$t('label.dns.delete.zone')} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
if (!jobId) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: 'Failed to get jobid for DeleteDnsZone',
duration: 0
})
this.loading = false
return
}
if (isDetailView) {
this.$router.push({ path: '/dnszone' })
} else {
this.$emit('refresh-data')
}
const onListPage = this.$route.path === '/dnszone'
this.$pollJob({
jobId: jobId,
title: this.$t('label.dns.delete.zone'),
description: this.resource.name,
successMethod: () => {
this.loading = false
this.$notification.success({
message: this.$t('label.dns.delete.zone'),
description: `${this.$t('message.success.delete.dns.zone')} ${this.resource.name}`
})
if (!onListPage) {
this.$router.push({ path: '/dnszone' })
}
},
errorMethod: () => {
this.loading = false
this.$notification.error({
message: this.$t('label.dns.delete.zone'),
description: `${this.$t('message.error.delete.dns.zone')} ${this.resource.name}`
})
},
loadingMessage: `${this.$t('label.dns.delete.zone')} ${this.resource.name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
action: { isFetchData: onListPage }
})
this.closeAction()
} catch (error) {
this.loading = false
this.$notification.error({
message: this.$t('message.request.failed'),
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},
closeAction () {

View File

@ -18,27 +18,31 @@
<template>
<div>
<a-spin :spinning="fetchLoading">
<a-button
shape="round"
style="float: right;margin-bottom: 10px; z-index: 8"
@click="() => { showAddForm = true }">
{{ $t('label.dns.create.record') }}
<plus-outlined style="margin-left: 5px;" />
</a-button>
<br />
<br />
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
<a-input-search
v-model:value="searchText"
:placeholder="$t('label.search')"
style="width: 250px;"
allow-clear />
<a-button
shape="round"
@click="() => { showAddForm = true }">
{{ $t('label.dns.create.record') }}
<plus-outlined style="margin-left: 5px;" />
</a-button>
</div>
<a-table
size="small"
style="overflow-y: auto; width: 100%;"
:columns="columns"
:dataSource="records"
:rowKey="item => item.id"
:pagination="false">
:dataSource="filteredRecords"
:rowKey="item => `${item.name}-${item.type}`"
:pagination="tablePagination">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'contents'">
<a-tag v-for="item in record.contents" :key="item">{{ item }}</a-tag>
<a-tag v-for="(item, idx) in record.contents" :key="idx">{{ item }}</a-tag>
</template>
<template v-if="column.dataIndex === 'ttl'">
{{ record.ttl }}
@ -59,24 +63,6 @@
</template>
</template>
</a-table>
<a-divider/>
<a-pagination
class="row-element pagination"
size="small"
:current="page"
:pageSize="pageSize"
:total="total"
:showTotal="total => `${$t('label.total')} ${total} ${$t('label.items')}`"
:pageSizeOptions="['10', '20', '40', '80', '100']"
@change="changePage"
@showSizeChange="changePageSize"
showSizeChanger>
<template #buildOptionText="props">
<span>{{ props.value }} / {{ $t('label.page') }}</span>
</template>
</a-pagination>
</a-spin>
<a-modal
@ -91,8 +77,7 @@
width="auto">
<CreateDnsRecord
:resource="resource"
@refresh-data="fetchData"
@close-action="showAddForm = false" />
@close-action="handleCloseCreateForm" />
</a-modal>
</div>
</template>
@ -121,19 +106,26 @@ export default {
data () {
return {
fetchLoading: false,
deleteLoading: false,
showAddForm: false,
total: 0,
searchText: '',
records: [],
page: 1,
pageSize: 10,
tablePagination: {
defaultPageSize: 10,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '40', '80', '100'],
showTotal: (total) => `${this.$t('label.total')} ${total} ${this.$t('label.items')}`
},
columns: [
{
title: this.$t('label.name'),
dataIndex: 'name'
dataIndex: 'name',
sorter: (a, b) => a.name.localeCompare(b.name)
},
{
title: this.$t('label.type'),
dataIndex: 'type'
dataIndex: 'type',
sorter: (a, b) => a.type.localeCompare(b.type)
},
{
title: this.$t('label.contents'),
@ -141,7 +133,8 @@ export default {
},
{
title: this.$t('label.ttl') + ' (' + this.$t('label.seconds') + ')',
dataIndex: 'ttl'
dataIndex: 'ttl',
sorter: (a, b) => a.ttl - b.ttl
},
{
key: 'actions',
@ -150,31 +143,36 @@ export default {
]
}
},
computed: {
filteredRecords () {
const q = this.searchText.trim().toLowerCase()
if (!q) return this.records
return this.records.filter(r =>
r.name?.toLowerCase().includes(q) ||
r.type?.toLowerCase().includes(q) ||
r.contents?.some(c => c.toLowerCase().includes(q))
)
}
},
created () {
this.fetchData()
},
watch: {
resource: {
deep: true,
handler (newItem) {
if (!newItem || !newItem.id) {
return
}
this.fetchData()
}
'resource.id' (newId) {
if (!newId) return
this.fetchData()
}
},
methods: {
fetchData () {
if (this.fetchLoading) return
const params = {
dnszoneid: this.resource.id,
page: this.page,
pagesize: this.pageSize
dnszoneid: this.resource.id
}
this.fetchLoading = true
getAPI('listDnsRecords', params).then(json => {
const response = json.listdnsrecordsresponse || {}
this.total = response.count || 0
this.records = response.dnsrecord || []
}).catch(error => {
this.$notifyError(error)
@ -183,15 +181,46 @@ export default {
})
},
handleDeleteRecord (record) {
if (this.deleteLoading) return
this.deleteLoading = true
const params = {
dnszoneid: this.resource.id,
name: record.name,
type: record.type
}
postAPI('deleteDnsRecord', params).then(() => {
this.$notification.success({
message: this.$t('message.success.delete.dns.record')
postAPI('deleteDnsRecord', params).then(response => {
const jobId = response?.deletednsrecordresponse?.jobid
if (!jobId) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: 'Failed to get jobid for DeleteDnsRecord',
duration: 0
})
this.deleteLoading = false
return
}
this.$pollJob({
jobId,
title: this.$t('label.dns.delete.record'),
description: record.name,
successMethod: () => {
this.$notification.success({
message: this.$t('message.success.delete.dns.record'),
description: `${this.$t('message.success.delete.dns.record')} ${record.name}`
})
this.records = this.records.filter(r => !(r.name === record.name && r.type === record.type))
this.deleteLoading = false
},
errorMethod: () => {
this.deleteLoading = false
},
loadingMessage: `${this.$t('label.dns.delete.record')} ${record.name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
action: {
isFetchData: false
}
})
}).catch(error => {
this.$notification.error({
@ -199,19 +228,34 @@ export default {
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
}).finally(() => {
this.fetchData()
this.deleteLoading = false
})
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
this.fetchData()
},
changePageSize (currentPage, pageSize) {
this.page = currentPage
this.pageSize = pageSize
this.fetchData()
handleCloseCreateForm (payload) {
this.showAddForm = false
const jobId = payload?.jobId
if (!jobId) return
const recordName = payload?.recordName || ''
this.$pollJob({
jobId,
title: this.$t('label.dns.create.record'),
description: recordName,
successMethod: () => {
this.$notification.success({
message: this.$t('label.dns.create.record'),
description: `${this.$t('message.success.create.dns.record')} ${recordName}`
})
this.fetchData()
},
errorMethod: () => {
this.fetchData()
},
loadingMessage: `${this.$t('label.dns.create.record')} ${recordName} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result'),
action: {
isFetchData: false
}
})
}
}
}

View File

@ -23,7 +23,7 @@
:model="form"
:rules="rules"
layout="vertical"
:disabled="loading">
@finish="handleSubmit">
<a-form-item name="name" ref="name">
<template #label>
@ -68,12 +68,14 @@
<a-input-password v-model:value="form.dnsapikey" :placeholder="apiParams.dnsapikey?.description" />
</a-form-item>
<a-form-item v-if="isAdminOrDomainAdmin()" name="publicdomainsuffix">
<template #label>
<tooltip-label :title="$t('label.dns.publicdomainsuffix')" :tooltip="apiParams.publicdomainsuffix?.description" />
</template>
<a-input v-model:value="form.publicdomainsuffix" :placeholder="apiParams.publicdomainsuffix?.description" />
</a-form-item>
<Transition name="slide-down">
<a-form-item v-if="isAdminOrDomainAdmin() && form.ispublic" name="publicdomainsuffix">
<template #label>
<tooltip-label :title="$t('label.dns.publicdomainsuffix')" :tooltip="apiParams.publicdomainsuffix?.description" />
</template>
<a-input v-model:value="form.publicdomainsuffix" :placeholder="apiParams.publicdomainsuffix?.description" />
</a-form-item>
</Transition>
<a-form-item name="nameservers" ref="nameservers">
<template #label>
@ -105,7 +107,7 @@
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>
@ -207,9 +209,9 @@ export default {
await postAPI('updateDnsServer', params)
this.$notification.success({
message: this.$t('label.dns.update.server'),
description: this.$t('message.success.update.dns.server')
description: `${this.$t('message.success.update.dns.server')} ${params.name}`
})
this.loading = false
this.$emit('refresh-data')
this.closeAction()
} catch (error) {
@ -218,7 +220,6 @@ export default {
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},
@ -304,4 +305,16 @@ export default {
.action-button button {
margin-left: 8px;
}
.slide-down-enter-active,
.slide-down-leave-active {
transition: max-height 0.3s ease, opacity 0.3s ease;
overflow: hidden;
max-height: 120px;
}
.slide-down-enter-from,
.slide-down-leave-to {
max-height: 0;
opacity: 0;
}
</style>

View File

@ -22,7 +22,8 @@
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
layout="vertical"
@finish="handleSubmit">
<a-form-item name="description" ref="description">
<template #label>
@ -43,7 +44,7 @@
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
htmlType="submit">
{{ $t('label.ok') }}
</a-button>
</div>
@ -96,13 +97,25 @@ export default {
this.loading = true
try {
await postAPI('updateDnsZone', { id: this.resource.id, description: this.form.description })
const response = await postAPI('updateDnsZone', {
id: this.resource.id,
description: this.form.description?.trim()
})
const zone = response?.updatednszoneresponse?.dnszone
if (!zone) {
this.$notification.error({
message: this.$t('message.request.failed'),
description: 'Failed to get updated DNS zone from response',
duration: 0
})
this.loading = false
return
}
this.$notification.success({
message: this.$t('label.dns.update.zone'),
description: this.$t('message.success.update.dns.zone')
description: `${this.$t('message.success.update.dns.zone')} ${this.resource.name}`
})
this.loading = false
this.$emit('refresh-data')
this.closeAction()
} catch (error) {
@ -111,7 +124,6 @@ export default {
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
} finally {
this.loading = false
}
},