UI related work and fixes in updateDnsServer cmd

This commit is contained in:
Manoj Kumar 2026-03-10 13:10:24 +05:30
parent 0df50cedf8
commit a9afbce186
No known key found for this signature in database
GPG Key ID: E952B7234D2C6F88
15 changed files with 1003 additions and 70 deletions

View File

@ -41,7 +41,7 @@ public interface DnsServer extends InternalIdentity, Identity, ControlledEntity
long getAccountId();
boolean isPublicServer();
boolean getPublicServer();
Date getCreated();

View File

@ -3640,7 +3640,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
if (caller.getId() == dnsServer.getAccountId()) {
return;
}
if (!dnsServer.isPublicServer()) {
if (!dnsServer.getPublicServer()) {
throw new PermissionDeniedException(caller + "is not allowed to access the DNS server " + dnsServer.getName());
}
Account owner = getAccount(dnsServer.getAccountId());

View File

@ -230,7 +230,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
dnsServer.setPort(cmd.getPort());
}
if (cmd.isPublic() != null) {
dnsServer.setIsPublic(cmd.isPublic());
dnsServer.setPublicServer(cmd.isPublic());
}
if (cmd.getPublicDomainSuffix() != null) {
@ -476,7 +476,7 @@ public class DnsProviderManagerImpl extends ManagerBase implements DnsProviderMa
Account caller = CallContext.current().getCallingAccount();
boolean isOwner = (server.getAccountId() == caller.getId());
if (!isOwner) {
if (!server.isPublicServer()) {
if (!server.getPublicServer()) {
throw new PermissionDeniedException("You do not have permission to use this DNS server.");
}
dnsZoneName = DnsProviderUtil.appendPublicSuffixToZone(dnsZoneName, server.getPublicDomainSuffix());

View File

@ -110,7 +110,7 @@ public class DnsServerDaoImpl extends GenericDaoBase<DnsServerVO, Long> implemen
sb.and().op(ApiConstants.ACCOUNT_ID, sb.entity().getAccountId(), SearchCriteria.Op.EQ);
if (!CollectionUtils.isEmpty(domainIds)) {
sb.or().op(ApiConstants.IS_PUBLIC, sb.entity().isPublicServer(), SearchCriteria.Op.EQ);
sb.or().op(ApiConstants.IS_PUBLIC, sb.entity().getPublicServer(), SearchCriteria.Op.EQ);
sb.and(ApiConstants.DOMAIN_IDS, sb.entity().getDomainId(), SearchCriteria.Op.IN);
sb.cp();
}

View File

@ -36,7 +36,6 @@ import javax.persistence.TemporalType;
import org.apache.cloudstack.dns.DnsProviderType;
import org.apache.cloudstack.dns.DnsServer;
import org.apache.cloudstack.dns.DnsZone;
import com.cloud.utils.StringUtils;
import com.cloud.utils.db.Encrypt;
@ -123,7 +122,7 @@ public class DnsServerVO implements DnsServer {
this.publicDomainSuffix = publicDomainSuffix;
this.publicServer = isPublic;
this.state = State.Enabled;
this.nameServers = String.join(",", nameServers);;
this.nameServers = String.join(",", nameServers);
}
@Override
@ -133,7 +132,7 @@ public class DnsServerVO implements DnsServer {
@Override
public Class<?> getEntityType() {
return DnsZone.class;
return DnsServer.class;
}
@Override
@ -176,7 +175,7 @@ public class DnsServerVO implements DnsServer {
return uuid;
}
public boolean isPublicServer() {
public boolean getPublicServer() {
return publicServer;
}
@ -202,8 +201,8 @@ public class DnsServerVO implements DnsServer {
this.nameServers = nameServers;
}
public void setIsPublic(boolean value) {
publicServer = value;
public void setPublicServer(boolean value) {
this.publicServer = value;
}
public void setPublicDomainSuffix(String publicDomainSuffix) {

View File

@ -292,11 +292,6 @@
"label.add.internal.lb": "Add internal LB",
"label.add.ip.range": "Add IP Range",
"label.add.ipv4.subnet": "Add IPv4 Subnet for Routed Networks",
"label.dns.server": "DNS Server",
"label.dns.add.server": "Add DNS Server",
"label.dns.update.server": "Update DNS Server",
"label.dns.delete.server": "Delete DNS Server",
"label.dnsrecords": "DNS Records",
"label.add.ip.v6.prefix": "Add IPv6 prefix",
"label.add.isolated.network": "Add Isolated Network",
"label.add.kubernetes.cluster": "Add Kubernetes Cluster",
@ -654,6 +649,7 @@
"label.console.proxy": "Console proxy",
"label.console.proxy.vm": "Console proxy VM",
"label.contains": "Contains",
"label.contents": "Contents",
"label.continue": "Continue",
"label.continue.install": "Continue with installation",
"label.controlnodes": "Control nodes",
@ -718,7 +714,6 @@
"label.created": "Created",
"label.creating": "Creating",
"label.creating.iprange": "Creating IP ranges",
"label.dns.credentials": "DNS API key",
"label.nameservers": "DNS Nameservers",
"label.credit": "Credit",
"label.cron": "Cron expression",
@ -943,9 +938,26 @@
"label.dns": "DNS",
"label.dns1": "DNS 1",
"label.dns2": "DNS 2",
"label.dns.add.record": "Add DNS Record",
"label.dns.add.server": "Add DNS Server",
"label.dns.add.zone": "Add DNS Zone",
"label.dns.credentials": "DNS API key",
"label.dns.delete.server": "Delete DNS Server",
"label.dns.delete.zone": "Delete DNS Zone",
"label.dns.records": "DNS Records",
"label.dns.record.name.tooltip": "The hostname or subdomain for this record (e.g. www)",
"label.dns.record.type.tooltip": "The DNS record type",
"label.dns.record.contents.tooltip": "The content values for this DNS record (type and press Enter to add)",
"label.dns.record.ttl.tooltip": "Time to live in seconds",
"label.dns.record.url": "Instance URL",
"label.dns.server": "DNS Server",
"label.dnsservername": "DNS Server name",
"label.dns.servers": "DNS Servers",
"label.dns.zone": "DNS Zone",
"label.dns.zones": "DNS Zones",
"label.dns.update.server": "Update DNS Server",
"label.dns.update.zone": "Update DNS Zone",
"label.dnsrecords": "DNS Records",
"label.domain": "Domain",
"label.domain.id": "Domain ID",
"label.domain.name": "Domain name",
@ -2594,6 +2606,7 @@
"label.two.factor.authentication.static.pin": "Your Two factor authentication static PIN",
"label.two.factor.authentication": "Two Factor Authentication",
"label.2FA": "2FA",
"label.ttl": "TTL",
"label.tungsten.fabric": "Tungsten Fabric",
"label.tungsten.fabric.provider": "Tungsten Fabric Provider",
"label.tungsten.fabric.routing": "Tungsten Fabric Routing",
@ -2954,7 +2967,9 @@
"message.action.delete.backup.schedule": "Please confirm that you want to delete this backup schedule?",
"message.action.delete.cluster": "Please confirm that you want to delete this Cluster.",
"message.action.delete.custom.action": "Please confirm that you want to delete this custom action.",
"message.confirm.delete.dns.record": "Are you sure you want to delete this DNS record?",
"message.action.delete.dns.server": "Please confirm you want to delete this DNS server.",
"message.action.delete.dns.zone": "Please confirm you want to delete this DNS zone.",
"message.action.delete.domain": "Please confirm that you want to delete this domain.",
"message.action.delete.extension": "Please confirm that you want to delete the extension",
"message.action.delete.external.firewall": "Please confirm that you would like to remove this external firewall. Warning: If you are planning to add back the same external firewall, you must reset usage data on the device.",
@ -3862,6 +3877,12 @@
"message.success.add.bgp.peer": "Successfully added new BGP peer",
"message.success.add.egress.rule": "Successfully added new egress rule",
"message.success.add.firewall.rule": "Successfully added new firewall rule",
"message.success.add.dns.server": "Successfully added DNS server",
"message.success.add.dns.zone": "Successfully added DNS zone",
"message.success.update.dns.server": "Successfully updated DNS server",
"message.success.update.dns.zone": "Successfully updated DNS zone",
"message.success.add.dns.record": "Successfully added DNS record",
"message.success.delete.dns.record": "Successfully deleted DNS record",
"message.success.add.guest.network": "Successfully created guest Network",
"message.success.add.gpu.device": "Successfully added GPU device",
"message.success.add.interface.static.route": "Successfully added interface Static Route",
@ -4158,6 +4179,9 @@
"migrate.from": "Migrate from",
"migrate.to": "Migrate to",
"migrationPolicy": "Migration policy",
"placeholder.dns.record.name": "e.g. www",
"placeholder.dns.record.type": "Select record type",
"placeholder.dns.record.contents": "Type a value and hit Enter",
"placeholder.quota.tariff.activationrule": "Quota tariff's activation rule",
"placeholder.quota.tariff.description": "Quota tariff's description",
"placeholder.quota.tariff.enddate": "Quota tariff's end date",

View File

@ -1219,7 +1219,7 @@ export default {
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation',
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering',
'/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', 'webhookfilters', '/quotatariff', '/sharedfs',
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule'].join('|'))
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule', '/dnsserver', '/dnszone'].join('|'))
.test(this.$route.path)
},
enableGroupAction () {

View File

@ -1488,47 +1488,15 @@ export default {
}
]
},
{
name: 'dnsrecords',
title: 'label.dns.records',
icon: 'global-outlined',
hidden: true,
permission: ['listDnsRecords'],
columns: ['name', 'url', 'provider'],
details: ['name', 'url', 'provider', 'ispublic', 'port', 'nameservers'],
related: [{
name: 'vm',
title: 'label.dns.zone',
param: 'dnszoneid'
}]
},
{
name: 'dnszones',
title: 'label.dns.zones',
icon: 'global-outlined',
hidden: true,
permission: ['listDnsZones'],
columns: ['name', 'state', 'dnsservername', 'dnsserveraccount'],
details: ['name', 'state', 'dnsservername', 'dnsserveraccount'],
tabs: [{
name: 'details',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
},
{
name: 'dnsrecords',
component: shallowRef(defineAsyncComponent(() => import('@/views/network/InternalLBAssignedVmTab.vue'))),
show: () => true
}]
},
{
name: 'dnsserver',
title: 'label.dns.server',
icon: 'global-outlined',
permission: ['listDnsServers'],
columns: ['name', 'url', 'provider'],
details: ['name', 'url', 'abc', 'provider', 'ispublic', 'port', 'nameservers', 'domain', 'account'],
details: ['name', 'url', 'provider', 'ispublic', 'port', 'nameservers', 'domain', 'account'],
related: [{
name: 'dnszones',
name: 'dnszone',
title: 'label.dns.zone',
param: 'dnsserverid'
}],
@ -1550,7 +1518,7 @@ export default {
label: 'label.dns.update.server',
dataView: true,
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/AddDnsServer.vue'))),
component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/UpdateDnsServer.vue'))),
show: (record) => { return true }
},
{
@ -1564,6 +1532,58 @@ export default {
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
},
{
name: 'dnszone',
title: 'label.dns.zones',
icon: 'global-outlined',
permission: ['listDnsZones'],
columns: ['name', 'state', 'dnsservername', 'account'],
details: ['name', 'id', 'state', 'dnsservername', 'dnsserverid', 'account', 'domainpath'],
tabs: [{
name: 'details',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
},
{
name: 'dns.records',
component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/DnsRecordsTab.vue'))),
show: () => true
}],
actions: [
{
api: 'createDnsZone',
icon: 'plus-outlined',
label: 'label.dns.add.zone',
listView: true,
popup: true,
disabled: (record) => false,
component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/AddDnsZone.vue'))),
show: () => {
return true
}
},
{
api: 'updateDnsZone',
icon: 'edit-outlined',
label: 'label.dns.update.zone',
dataView: true,
popup: true,
disabled: (record) => false,
component: shallowRef(defineAsyncComponent(() => import('@/views/network/dns/UpdateDnsZone.vue'))),
show: (record) => { return true }
},
{
api: 'deleteDnsZone',
icon: 'delete-outlined',
label: 'label.dns.delete.zone',
message: 'message.action.delete.dns.zone',
dataView: true,
groupAction: true,
disabled: (record) => false,
show: (record) => { return true },
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
}
]
}

View File

@ -58,8 +58,8 @@
{{ record.isolationuri }}
</a-descriptions-item>
<a-descriptions-item :label="$t('label.dns_url')" v-if="record.dns_url">
{{ record.dns_url }}
<a-descriptions-item :label="$t('label.dns.record.url')" v-if="record.dnsrecordurl">
{{ record.dnsrecordurl }}
</a-descriptions-item>
</template>
</a-descriptions>

View File

@ -0,0 +1,192 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label
:title="$t('label.name')"
:tooltip="$t('label.dns.record.name.tooltip')" />
</template>
<a-input
v-model:value="form.name"
:placeholder="$t('placeholder.dns.record.name')"
v-focus="true" />
</a-form-item>
<a-form-item name="type" ref="type">
<template #label>
<tooltip-label
:title="$t('label.type')"
:tooltip="$t('label.dns.record.type.tooltip')" />
</template>
<a-select
v-model:value="form.type"
:placeholder="$t('placeholder.dns.record.type')"
showSearch>
<a-select-option
v-for="rtype in recordTypes"
:key="rtype"
:value="rtype">
{{ rtype }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="contents" ref="contents">
<template #label>
<tooltip-label
:title="$t('label.contents')"
:tooltip="$t('label.dns.record.contents.tooltip')" />
</template>
<a-select
v-model:value="form.contents"
mode="tags"
:placeholder="$t('placeholder.dns.record.contents')"
style="width: 100%"
:token-separators="[',']" />
</a-form-item>
<a-form-item name="ttl" ref="ttl">
<template #label>
<tooltip-label
:title="$t('label.ttl')"
:tooltip="$t('label.dns.record.ttl.tooltip')" />
</template>
<a-input-number
v-model:value="form.ttl"
:min="1"
:max="2147483647"
style="width: 100%" />
</a-form-item>
<div class="action-button">
<a-button @click="closeAction">
{{ $t('label.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
{{ $t('label.ok') }}
</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
name: 'AddDnsRecord',
components: {
TooltipLabel
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
loading: false,
form: {
name: '',
type: 'A',
contents: [],
ttl: 3600
},
rules: {},
recordTypes: ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SRV', 'PTR', 'NS']
}
},
created () {
this.rules = {
name: [{ required: true, message: this.$t('message.error.required.input') }],
type: [{ required: true, message: this.$t('message.error.required.input') }],
contents: [{ required: true, type: 'array', min: 1, message: this.$t('message.error.required.input') }],
ttl: [{ required: true, message: this.$t('message.error.required.input') }]
}
},
methods: {
async handleSubmit () {
if (this.loading) return
try {
await this.$refs.formRef.validate()
} catch (error) {
if (error.errorFields && error.errorFields.length > 0) {
this.$refs.formRef.scrollToField(error.errorFields[0].name)
}
return
}
this.loading = true
try {
await postAPI('addDnsRecord', {
dnszoneid: this.resource.id,
...this.form
})
this.$notification.success({
message: this.$t('label.dns.add.record'),
description: this.$t('message.success.add.dns.record')
})
this.$emit('refresh-data')
this.closeAction()
} 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')
}
}
}
</script>
<style lang="less" scoped>
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 450px;
}
}
.action-button {
text-align: right;
margin-top: 20px;
}
.action-button button {
margin-left: 8px;
}
</style>

View File

@ -97,14 +97,18 @@
:title="$t('label.nameservers')"
:tooltip="apiParams.nameservers?.description" />
</template>
<a-input
<a-select
v-model:value="form.nameservers"
:placeholder="apiParams.nameservers?.description" />
mode="tags"
style="width: 100%"
:token-separators="[',', ' ']"
:placeholder="apiParams.nameservers?.description || 'ns1.example.com, ns2.example.com'" />
</a-form-item>
<a-form-item name="ispublic" ref="ispublic">
<a-checkbox v-model:checked="form.ispublic">
{{ "Public server" }}
{{ $t('label.ispublic') }}
</a-checkbox>
</a-form-item>
@ -163,32 +167,21 @@ export default {
methods: {
async handleSubmit () {
if (this.loading) return
// 1. Validate the form natively using Vue 3 $refs
try {
await this.$refs.formRef.validate()
} catch (error) {
// Scroll to the first field with an error
if (error.errorFields && error.errorFields.length > 0) {
this.$refs.formRef.scrollToField(error.errorFields[0].name)
}
return // Abort submission
return
}
this.loading = true
// 2. Execute the API
try {
console.log('form data ', this.form)
// Pass the form object directly
await postAPI('addDnsServer', this.form)
this.$notification.success({
message: this.$t('label.add.dns.server'),
description: this.$t('message.success.add.dns.server')
})
// Refresh the parent table and close modal
this.$emit('refresh-data')
this.closeAction()
} catch (error) {

View File

@ -0,0 +1,179 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label
:title="$t('label.name')"
:tooltip="apiParams.name?.description" />
</template>
<a-input
v-model:value="form.name"
:placeholder="apiParams.name?.description || 'e.g. example.com'"
v-focus="true" />
</a-form-item>
<a-form-item name="dnsserverid" ref="dnsserverid">
<template #label>
<tooltip-label
:title="$t('label.dns.server')"
:tooltip="apiParams.dnsserverid?.description" />
</template>
<a-select
v-model:value="form.dnsserverid"
:placeholder="apiParams.dnsserverid?.description || 'Select DNS Server'"
:loading="fetchingServers"
showSearch
optionFilterProp="label">
<a-select-option
v-for="server in dnsServers"
:key="server.id"
:value="server.id"
:label="server.name">
{{ server.name }}
</a-select-option>
</a-select>
</a-form-item>
<div class="action-button">
<a-button @click="closeAction">
{{ $t('label.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
{{ $t('label.ok') }}
</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
name: 'AddDnsZone',
components: {
TooltipLabel
},
data () {
return {
loading: false,
apiParams: {},
form: {
name: '',
dnsserverid: undefined
},
rules: {},
fetchingServers: false,
dnsServers: []
}
},
created () {
this.apiParams = this.$getApiParams('createDnsZone') || {}
this.rules = {
name: [{ required: true, message: this.$t('message.error.required.input') }],
dnsserverid: [{ required: true, message: this.$t('message.error.required.input') }]
}
this.fetchDnsServers()
},
methods: {
async handleSubmit () {
if (this.loading) return
try {
await this.$refs.formRef.validate()
} catch (error) {
const field = error?.errorFields?.[0]?.name
if (field) {
this.$refs.formRef.scrollToField(field)
}
return
}
this.loading = true
try {
await postAPI('createDnsZone', this.form)
this.$notification.success({
message: this.$t('label.dns.add.zone'),
description: this.$t('message.success.add.dns.zone')
})
this.$emit('refresh-data')
this.closeAction()
} 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 fetchDnsServers () {
this.fetchingServers = true
try {
const response = await getAPI('listDnsServers')
const listResponse = response?.listdnsserversresponse || {}
this.dnsServers = listResponse.dnsserver || []
if (this.dnsServers.length > 0) {
this.form.dnsserverid = this.dnsServers[0].id
}
} catch (error) {
console.error('Failed to fetch DNS servers', error)
this.$message.warning('Could not load DNS servers.')
} finally {
this.fetchingServers = false
}
}
}
}
</script>
<style lang="less" scoped>
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 450px;
}
}
.action-button {
text-align: right;
margin-top: 20px;
}
.action-button button {
margin-left: 8px;
}
</style>

View File

@ -0,0 +1,209 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<a-spin :spinning="fetchLoading">
<a-button
shape="round"
style="float: right;margin-bottom: 10px; z-index: 8"
@click="() => { showAddForm = true }">
<template #icon><plus-outlined /></template>
{{ $t('label.dns.add.record') }}
</a-button>
<br />
<br />
<a-table
size="small"
style="overflow-y: auto; width: 100%;"
:columns="columns"
:dataSource="records"
:rowKey="item => item.id"
:pagination="false">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'contents'">
<a-tag v-for="item in record.contents" :key="item">{{ item }}</a-tag>
</template>
<template v-if="column.key === 'actions'">
<a-popconfirm
:title="$t('message.confirm.delete.dns.record')"
@confirm="handleDeleteRecord(record)"
:okText="$t('label.yes')"
:cancelText="$t('label.no')">
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.delete')"
type="primary"
:danger="true"
icon="delete-outlined" />
</a-popconfirm>
</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
v-if="showAddForm"
:visible="showAddForm"
:title="$t('label.dns.add.record')"
:maskClosable="false"
:closable="true"
:footer="null"
@cancel="() => { showAddForm = false }"
centered
width="auto">
<AddDnsRecord
:resource="resource"
@refresh-data="fetchData"
@close-action="showAddForm = false" />
</a-modal>
</div>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import TooltipButton from '@/components/widgets/TooltipButton'
import AddDnsRecord from '@/views/network/dns/AddDnsRecord'
export default {
name: 'DnsRecordsTab',
components: {
TooltipButton,
AddDnsRecord
},
props: {
resource: {
type: Object,
required: true
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
fetchLoading: false,
showAddForm: false,
total: 0,
records: [],
page: 1,
pageSize: 10,
columns: [
{
title: this.$t('label.name'),
dataIndex: 'name'
},
{
title: this.$t('label.type'),
dataIndex: 'type'
},
{
title: this.$t('label.contents'),
dataIndex: 'contents'
},
{
title: this.$t('label.ttl'),
dataIndex: 'ttl'
},
{
key: 'actions',
title: this.$t('label.actions')
}
]
}
},
created () {
this.fetchData()
},
watch: {
resource: {
deep: true,
handler (newItem) {
if (!newItem || !newItem.id) {
return
}
this.fetchData()
}
}
},
methods: {
fetchData () {
const params = {
dnszoneid: this.resource.id,
page: this.page,
pagesize: this.pageSize
}
this.fetchLoading = true
getAPI('listDnsRecords', params).then(json => {
const response = json.listdnsrecordsresponse || {}
this.total = response.count || 0
this.records = response.dnsrecord || []
}).catch(error => {
console.error('Failed to fetch DNS records', error)
}).finally(() => {
this.fetchLoading = false
})
},
handleDeleteRecord (record) {
postAPI('deleteDnsRecord', { id: record.id }).then(() => {
this.$notification.success({
message: this.$t('message.success.delete.dns.record')
})
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: error?.response?.headers['x-description'] || error.message,
duration: 0
})
}).finally(() => {
this.fetchData()
})
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
this.fetchData()
},
changePageSize (currentPage, pageSize) {
this.page = currentPage
this.pageSize = pageSize
this.fetchData()
}
}
}
</script>

View File

@ -0,0 +1,173 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label
:title="$t('label.name')"
:tooltip="apiParams.name?.description" />
</template>
<a-input
v-model:value="form.name"
:placeholder="apiParams.name?.description"
v-focus="true" />
</a-form-item>
<a-form-item name="url" ref="url">
<template #label>
<tooltip-label
:title="$t('label.url')"
:tooltip="apiParams.url?.description" />
</template>
<a-input
v-model:value="form.url"
:placeholder="apiParams.url?.description" />
</a-form-item>
<a-form-item name="port" ref="port">
<template #label>
<tooltip-label
:title="$t('label.port')"
:tooltip="apiParams.port?.description" />
</template>
<a-input-number
v-model:value="form.port"
:min="1"
:max="65535"
style="width: 100%" />
</a-form-item>
<div class="action-button">
<a-button @click="closeAction">
{{ $t('label.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
{{ $t('label.ok') }}
</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
name: 'UpdateDnsServer',
components: {
TooltipLabel
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
loading: false,
apiParams: {},
form: {
name: '',
url: '',
port: 53
},
rules: {}
}
},
created () {
this.apiParams = this.$getApiParams('updateDnsServer') || {}
this.rules = {
name: [{ required: true, message: this.$t('message.error.required.input') }],
url: [{ required: true, message: this.$t('message.error.required.input') }]
}
this.form.name = this.resource.name || ''
this.form.url = this.resource.url || ''
this.form.port = this.resource.port || 53
},
methods: {
async handleSubmit () {
if (this.loading) return
try {
await this.$refs.formRef.validate()
} catch (error) {
if (error.errorFields && error.errorFields.length > 0) {
this.$refs.formRef.scrollToField(error.errorFields[0].name)
}
return
}
this.loading = true
try {
await postAPI('updateDnsServer', { id: this.resource.id, ...this.form })
this.$notification.success({
message: this.$t('label.dns.update.server'),
description: this.$t('message.success.update.dns.server')
})
this.$emit('refresh-data')
this.closeAction()
} 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')
}
}
}
</script>
<style lang="less" scoped>
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 450px;
}
}
.action-button {
text-align: right;
margin-top: 20px;
}
.action-button button {
margin-left: 8px;
}
</style>

View File

@ -0,0 +1,144 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-spin :spinning="loading">
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
<a-form-item name="name" ref="name">
<template #label>
<tooltip-label
:title="$t('label.name')"
:tooltip="apiParams.name?.description" />
</template>
<a-input
v-model:value="form.name"
:placeholder="apiParams.name?.description"
v-focus="true" />
</a-form-item>
<div class="action-button">
<a-button @click="closeAction">
{{ $t('label.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
@click="handleSubmit">
{{ $t('label.ok') }}
</a-button>
</div>
</a-form>
</a-spin>
</div>
</template>
<script>
import { postAPI } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
name: 'UpdateDnsZone',
components: {
TooltipLabel
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
loading: false,
apiParams: {},
form: {
name: ''
},
rules: {}
}
},
created () {
this.apiParams = this.$getApiParams('updateDnsZone') || {}
this.rules = {
name: [{ required: true, message: this.$t('message.error.required.input') }]
}
this.form.name = this.resource.name || ''
},
methods: {
async handleSubmit () {
if (this.loading) return
try {
await this.$refs.formRef.validate()
} catch (error) {
if (error.errorFields && error.errorFields.length > 0) {
this.$refs.formRef.scrollToField(error.errorFields[0].name)
}
return
}
this.loading = true
try {
await postAPI('updateDnsZone', { id: this.resource.id, name: this.form.name })
this.$notification.success({
message: this.$t('label.dns.update.zone'),
description: this.$t('message.success.update.dns.zone')
})
this.$emit('refresh-data')
this.closeAction()
} 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')
}
}
}
</script>
<style lang="less" scoped>
.form-layout {
width: 80vw;
@media (min-width: 600px) {
width: 450px;
}
}
.action-button {
text-align: right;
margin-top: 20px;
}
.action-button button {
margin-left: 8px;
}
</style>