This is an automated email from the ASF dual-hosted git repository.
dahn pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new 3d7d412d5be UI: Add comprehensive domain deletion confirmation dialog
(Feature Request #11497) (#12380)
3d7d412d5be is described below
commit 3d7d412d5bedf77c8994553562a1ba2c0c65cfbe
Author: Imvedansh <[email protected]>
AuthorDate: Thu Feb 5 16:22:11 2026 +0530
UI: Add comprehensive domain deletion confirmation dialog (Feature Request
#11497) (#12380)
---
ui/public/locales/en.json | 4 +
ui/src/components/view/DomainDeleteConfirm.vue | 155 +++++++++++++++++++++++++
ui/src/views/iam/DomainView.vue | 56 ++++++++-
3 files changed, 212 insertions(+), 3 deletions(-)
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 99873820d53..673f6da0ad1 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -935,6 +935,7 @@
"label.endpoint": "Endpoint",
"label.endport": "End port",
"label.enter.account.name": "Enter the account name",
+"label.enter.domain.name": "Enter the domain name",
"label.enter.code": "Enter 2FA code to verify",
"label.enter.static.pin": "Enter static PIN to verify",
"label.enter.token": "Enter token",
@@ -2973,6 +2974,9 @@
"message.delete.account.processing": "Deleting account",
"message.delete.account.success": "Successfully deleted account",
"message.delete.account.warning": "Deleting this account will delete all of
the instances, volumes and snapshots associated with the account.",
+"message.delete.domain.confirm": "Please confirm that you want to delete this
domain by entering the name of the domain below.",
+"message.delete.domain.warning": "All associated accounts, users, VMs, and
sub-domains will be permanently deleted. This action cannot be undone.",
+"message.delete.domain.failed": "Delete domain failed",
"message.delete.acl.processing": "Removing ACL rule...",
"message.delete.acl.rule": "Remove ACL rule",
"message.delete.acl.rule.failed": "Failed to remove ACL rule.",
diff --git a/ui/src/components/view/DomainDeleteConfirm.vue
b/ui/src/components/view/DomainDeleteConfirm.vue
new file mode 100644
index 00000000000..4fdb9b658ad
--- /dev/null
+++ b/ui/src/components/view/DomainDeleteConfirm.vue
@@ -0,0 +1,155 @@
+// 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>
+ <a-modal
+ :visible="true"
+ :title="$t('label.action.delete.domain') + ': ' + domain.name"
+ :okText="$t('label.delete.domain')"
+ okType="danger"
+ :confirmLoading="loading"
+ :ok-button-props="{ disabled: !canDelete }"
+ @cancel="emitClose"
+ @ok="emitConfirm">
+
+ <a-alert
+ type="warning"
+ show-icon
+ style="margin-bottom: 16px">
+ <template #message>
+ <div v-html="$t('message.delete.domain.warning')"></div>
+ </template>
+ </a-alert>
+
+ <a-spin v-if="loading" />
+
+ <a-table
+ v-else
+ size="small"
+ :columns="columns"
+ :dataSource="accountVmSummary"
+ :pagination="false"
+ rowKey="account" />
+
+ <div style="margin-top: 16px">
+ <a-alert style="margin-bottom: 10px">
+ <template #message>
+ <div v-html="$t('message.delete.domain.confirm')"></div>
+ </template>
+ </a-alert>
+ <a-input
+ v-model:value="confirmText"
+ :placeholder="$t('label.enter.domain.name')" />
+ </div>
+
+ </a-modal>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ name: 'DomainDeleteConfirm',
+ props: {
+ domain: {
+ type: Object,
+ required: true
+ }
+ },
+ data () {
+ return {
+ loading: false,
+ confirmText: '',
+ accountVmSummary: []
+ }
+ },
+ computed: {
+ canDelete () {
+ return this.confirmText.trim() === this.domain.name.trim()
+ },
+ columns () {
+ return [
+ { title: this.$t('label.account'), dataIndex: 'account' },
+ { title: this.$t('label.total') + ' VMs', dataIndex: 'total' },
+ { title: this.$t('label.running') + ' VMs', dataIndex: 'running' },
+ { title: this.$t('label.stopped') + ' VMs', dataIndex: 'stopped' }
+ ]
+ }
+ },
+ mounted () {
+ this.fetchDomainImpact()
+ },
+ methods: {
+ emitClose () {
+ this.$emit('close')
+ },
+ emitConfirm () {
+ if (this.canDelete) {
+ this.$emit('confirm')
+ }
+ },
+ async fetchDomainImpact () {
+ this.loading = true
+ try {
+ const accResp = await api('listAccounts', {
+ domainid: this.domain.id,
+ listall: true
+ })
+
+ const accounts =
+ accResp.listaccountsresponse &&
+ accResp.listaccountsresponse.account
+ ? accResp.listaccountsresponse.account
+ : []
+
+ const vmResp = await api('listVirtualMachines', {
+ domainid: this.domain.id,
+ listall: true
+ })
+
+ const vms =
+ vmResp.listvirtualmachinesresponse &&
+ vmResp.listvirtualmachinesresponse.virtualmachine
+ ? vmResp.listvirtualmachinesresponse.virtualmachine
+ : []
+
+ this.accountVmSummary = accounts.map(account => {
+ const accountVms = vms.filter(vm => vm.account === account.name)
+ const running = accountVms.filter(vm => vm.state ===
'Running').length
+ const stopped = accountVms.length - running
+
+ return {
+ account: account.name,
+ total: accountVms.length,
+ running,
+ stopped
+ }
+ })
+ } catch (e) {
+ this.$notification.error({
+ message: this.$t('message.request.failed'),
+ description: e.response?.headers['x-description'] ||
this.$t('message.request.failed')
+ })
+ } finally {
+ this.loading = false
+ }
+ }
+ }
+}
+</script>
+
+<style scoped>
+</style>
diff --git a/ui/src/views/iam/DomainView.vue b/ui/src/views/iam/DomainView.vue
index ed875151a13..2b75d836b13 100644
--- a/ui/src/views/iam/DomainView.vue
+++ b/ui/src/views/iam/DomainView.vue
@@ -74,6 +74,11 @@
:resource="resource"
:action="action"/>
</div>
+ <domain-delete-confirm
+ v-if="showDeleteConfirm"
+ :domain="deleteDomainResource"
+ @close="showDeleteConfirm = false"
+ @confirm="confirmDeleteDomain" />
</div>
</template>
@@ -87,6 +92,7 @@ import ActionButton from '@/components/view/ActionButton'
import TreeView from '@/components/view/TreeView'
import DomainActionForm from '@/views/iam/DomainActionForm'
import ResourceView from '@/components/view/ResourceView'
+import DomainDeleteConfirm from '@/components/view/DomainDeleteConfirm'
import eventBus from '@/config/eventBus'
export default {
@@ -96,7 +102,8 @@ export default {
ActionButton,
TreeView,
DomainActionForm,
- ResourceView
+ ResourceView,
+ DomainDeleteConfirm
},
mixins: [mixinDevice],
data () {
@@ -111,7 +118,9 @@ export default {
action: {},
dataView: false,
domainStore: {},
- treeDeletedKey: null
+ treeDeletedKey: null,
+ showDeleteConfirm: false,
+ deleteDomainResource: null
}
},
computed: {
@@ -205,7 +214,12 @@ export default {
})
},
execAction (action) {
- this.treeDeletedKey = action.api === 'deleteDomain' ? this.resource.key
: null
+ if (action.api === 'deleteDomain') {
+ this.deleteDomainResource = this.resource
+ this.showDeleteConfirm = true
+ return
+ }
+ this.treeDeletedKey = null
this.actionData = []
this.action = action
this.action.params = store.getters.apis[this.action.api].params
@@ -319,6 +333,42 @@ export default {
closeAction () {
this.showAction = false
},
+ confirmDeleteDomain () {
+ const domain = this.deleteDomainResource
+ const params = { id: domain.id, cleanup: true }
+
+ api('deleteDomain', params).then(json => {
+ const jobId = json.deletedomainresponse.jobid
+
+ this.$pollJob({
+ jobId,
+ title: this.$t('label.action.delete.domain'),
+ description: domain.name,
+ loadingMessage: `${this.$t('label.action.delete.domain')}
${domain.name}`,
+ successMessage: `${this.$t('label.action.delete.domain')}
${domain.name}`,
+ catchMessage: this.$t('error.fetching.async.job.result'),
+ successMethod: () => {
+ this.$router.replace({ path: '/domain' })
+ this.resource = {}
+ this.treeSelected = {}
+ this.treeDeletedKey = null
+ this.treeViewKey += 1
+ this.$nextTick(() => {
+ this.fetchData()
+ })
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: this.$t('message.request.failed'),
+ description: error.response?.headers['x-description'] ||
this.$t('message.request.failed')
+ })
+ }).finally(() => {
+ this.showDeleteConfirm = false
+ this.deleteDomainResource = null
+ this.treeDeletedKey = null
+ })
+ },
forceRerender () {
this.treeViewKey += 1
}