mirror of https://github.com/apache/cloudstack.git
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:
parent
2aae36d3e1
commit
e7b7b4cbbb
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue