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
     }

Reply via email to