Copilot commented on code in PR #13225:
URL: https://github.com/apache/cloudstack/pull/13225#discussion_r3311029380


##########
ui/src/components/view/ApiKeyPairsTab.vue:
##########
@@ -0,0 +1,456 @@
+// 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
+        v-if="'registerUserKeys' in $store.getters.apis"
+        type="dashed"
+        style="width: 100%; margin-bottom: 15px"
+        @click="onShowAddKeyPair()">
+        <template #icon><plus-outlined /></template>
+        {{ $t('label.register.api.key') }}
+      </a-button>
+      <a-button
+        v-if="this.selectedRowKeys.length > 0 && ('deleteUserKeys' in 
$store.getters.apis)"
+        type="primary"
+        danger
+        style="width: 100%; margin-bottom: 15px"
+        @click="bulkActionConfirmation()">
+        <template #icon><delete-outlined /></template>
+        {{ $t('label.action.bulk.delete.api.keys') }}
+      </a-button>
+      <a-table
+        size="small"
+        style="overflow-y: auto"
+        :columns="columns"
+        :dataSource="keypairs"
+        :rowKey="item => item.id"
+        :rowSelection="rowSelection()"
+        :pagination="false" >
+        <template #name="{ record }">
+          <div>
+            <router-link :to="{ path: '/keypair/' + record.id }" >
+              {{ record.name }}
+            </router-link>
+          </div>
+        </template>
+        <template #apikey="{ record }">
+          <strong>
+            <tooltip-button
+              tooltipPlacement="right"
+              :tooltip="$t('label.copy')"
+              icon="CopyOutlined"
+              type="dashed"
+              size="small"
+              @onClick="$message.success($t('label.copied.clipboard'))"
+              :copyResource="record.apikey" />
+          </strong>
+          <div>
+            {{ record.apikey.substring(0, 20) }}...
+          </div>
+        </template>
+
+        <template #secretkey="{ record }">
+          <strong>
+            <tooltip-button
+              tooltipPlacement="right"
+              :tooltip="$t('label.copy')"
+              icon="CopyOutlined"
+              type="dashed"
+              size="small"
+              @onClick="$message.success($t('label.copied.clipboard'))"
+              :copyResource="record.secretkey" />
+          </strong>
+          <div>
+            {{ record.secretkey.substring(0, 20) }}...
+          </div>
+        </template>
+
+        <template #startdate="{ record }">
+          <div> {{ $toLocaleDate(record.startdate) }} </div>
+        </template>
+
+        <template #enddate="{ record }">
+          <div> {{ $toLocaleDate(record.enddate)}} </div>
+        </template>
+
+        <template #created="{ record }">
+          <div> {{ $toLocaleDate(record.created) }} </div>
+        </template>
+
+      </a-table>
+      <a-divider/>
+      <a-pagination
+        class="row-element pagination"
+        size="small"
+        :current="page"
+        :pageSize="pageSize"
+        :total="totalKeypairs"
+        :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>
+    <bulk-action-view
+      v-if="(showConfirmationAction || showGroupActionModal)"
+      :showConfirmationAction="showConfirmationAction"
+      :showGroupActionModal="showGroupActionModal"
+      :items="keypairs"
+      :selectedRowKeys="selectedRowKeys"
+      :selectedItems="selectedItems"
+      :columns="columns"
+      :selectedColumns="selectedColumns"
+      action="eraseKeypairs"
+      :loading="loading"
+      :message="bulkDeleteMessage"
+      @group-action="eraseKeypairs"
+      @handle-cancel="handleCancelBulk"
+      @close-modal="closeModalBulk" />
+    <generate-api-key-pair
+      :showAddKeyPair="showAddKeyPair"
+      :resource="resource"
+      @fetch-data="fetchData"
+      @refresh-data="handleRefreshData"
+      @close-modal="closeModalAddKeyPair" />
+  </div>
+</template>
+<script>
+import { getAPI, postAPI } from '@/api'
+import Status from '@/components/widgets/Status'
+import TooltipButton from '@/components/widgets/TooltipButton'
+import BulkActionView from '@/components/view/BulkActionView.vue'
+import eventBus from '@/config/eventBus'
+import OwnershipSelection from '@/views/compute/wizard/OwnershipSelection.vue'
+import GenerateApiKeyPair from '@/views/iam/GenerateApiKeyPair.vue'
+
+export default {
+  name: 'ApiKeyPairsTab',
+  components: {
+    OwnershipSelection,
+    Status,
+    TooltipButton,
+    BulkActionView,
+    GenerateApiKeyPair
+  },

Review Comment:
   `OwnershipSelection` and `Status` are imported and registered as components 
but never used in the template. Please drop the unused imports/components to 
avoid lint warnings and reduce bundle size.



##########
ui/src/components/view/ApiKeyPairsTab.vue:
##########
@@ -0,0 +1,456 @@
+// 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
+        v-if="'registerUserKeys' in $store.getters.apis"
+        type="dashed"
+        style="width: 100%; margin-bottom: 15px"
+        @click="onShowAddKeyPair()">
+        <template #icon><plus-outlined /></template>
+        {{ $t('label.register.api.key') }}
+      </a-button>
+      <a-button
+        v-if="this.selectedRowKeys.length > 0 && ('deleteUserKeys' in 
$store.getters.apis)"
+        type="primary"
+        danger
+        style="width: 100%; margin-bottom: 15px"
+        @click="bulkActionConfirmation()">
+        <template #icon><delete-outlined /></template>
+        {{ $t('label.action.bulk.delete.api.keys') }}
+      </a-button>
+      <a-table
+        size="small"
+        style="overflow-y: auto"
+        :columns="columns"
+        :dataSource="keypairs"
+        :rowKey="item => item.id"
+        :rowSelection="rowSelection()"
+        :pagination="false" >
+        <template #name="{ record }">
+          <div>
+            <router-link :to="{ path: '/keypair/' + record.id }" >
+              {{ record.name }}
+            </router-link>
+          </div>
+        </template>
+        <template #apikey="{ record }">
+          <strong>
+            <tooltip-button
+              tooltipPlacement="right"
+              :tooltip="$t('label.copy')"
+              icon="CopyOutlined"
+              type="dashed"
+              size="small"
+              @onClick="$message.success($t('label.copied.clipboard'))"
+              :copyResource="record.apikey" />
+          </strong>
+          <div>
+            {{ record.apikey.substring(0, 20) }}...
+          </div>
+        </template>
+
+        <template #secretkey="{ record }">
+          <strong>
+            <tooltip-button
+              tooltipPlacement="right"
+              :tooltip="$t('label.copy')"
+              icon="CopyOutlined"
+              type="dashed"
+              size="small"
+              @onClick="$message.success($t('label.copied.clipboard'))"
+              :copyResource="record.secretkey" />
+          </strong>
+          <div>
+            {{ record.secretkey.substring(0, 20) }}...
+          </div>
+        </template>
+
+        <template #startdate="{ record }">
+          <div> {{ $toLocaleDate(record.startdate) }} </div>
+        </template>
+
+        <template #enddate="{ record }">
+          <div> {{ $toLocaleDate(record.enddate)}} </div>
+        </template>
+
+        <template #created="{ record }">
+          <div> {{ $toLocaleDate(record.created) }} </div>
+        </template>
+
+      </a-table>
+      <a-divider/>
+      <a-pagination
+        class="row-element pagination"
+        size="small"
+        :current="page"
+        :pageSize="pageSize"
+        :total="totalKeypairs"
+        :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>
+    <bulk-action-view
+      v-if="(showConfirmationAction || showGroupActionModal)"
+      :showConfirmationAction="showConfirmationAction"
+      :showGroupActionModal="showGroupActionModal"
+      :items="keypairs"
+      :selectedRowKeys="selectedRowKeys"
+      :selectedItems="selectedItems"
+      :columns="columns"
+      :selectedColumns="selectedColumns"
+      action="eraseKeypairs"
+      :loading="loading"
+      :message="bulkDeleteMessage"
+      @group-action="eraseKeypairs"
+      @handle-cancel="handleCancelBulk"
+      @close-modal="closeModalBulk" />
+    <generate-api-key-pair
+      :showAddKeyPair="showAddKeyPair"
+      :resource="resource"
+      @fetch-data="fetchData"
+      @refresh-data="handleRefreshData"
+      @close-modal="closeModalAddKeyPair" />
+  </div>
+</template>
+<script>
+import { getAPI, postAPI } from '@/api'
+import Status from '@/components/widgets/Status'
+import TooltipButton from '@/components/widgets/TooltipButton'
+import BulkActionView from '@/components/view/BulkActionView.vue'
+import eventBus from '@/config/eventBus'
+import OwnershipSelection from '@/views/compute/wizard/OwnershipSelection.vue'
+import GenerateApiKeyPair from '@/views/iam/GenerateApiKeyPair.vue'
+
+export default {
+  name: 'ApiKeyPairsTab',
+  components: {
+    OwnershipSelection,
+    Status,
+    TooltipButton,
+    BulkActionView,
+    GenerateApiKeyPair
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      fetchLoading: false,
+      keypairs: [],
+      page: 1,
+      pageSize: 10,
+      totalKeypairs: 0,
+      selectedRowKeys: [],
+      selectedItems: [],
+      selectedColumns: [],
+      filterColumns: ['Action'],
+      showConfirmationAction: false,
+      showAddKeyPair: false,
+      showGroupActionModal: false,
+      bulkDeleteMessage: {
+        title: this.$t('label.action.bulk.delete.api.keys'),
+        confirmMessage: this.$t('label.confirm.delete.api.keys')
+      },
+      columns: [
+        {
+          title: this.$t('label.name'),
+          dataIndex: 'name',
+          slots: { customRender: 'name' }
+        },
+        {
+          title: this.$t('label.apikey'),
+          dataIndex: 'apikey',
+          slots: { customRender: 'apikey' }
+        },
+        {
+          title: this.$t('label.secretkey'),
+          dataIndex: 'secretkey',
+          slots: { customRender: 'secretkey' }
+        },
+        {
+          title: this.$t('label.start.date'),
+          dataIndex: 'startdate',
+          slots: { customRender: 'startdate' }
+        },
+        {
+          title: this.$t('label.end.date'),
+          dataIndex: 'enddate',
+          slots: { customRender: 'enddate' }
+        },
+        {
+          title: this.$t('label.created'),
+          dataIndex: 'created',
+          slots: { customRender: 'created' }
+        }
+      ]
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  watch: {
+    resource: {
+      deep: true,
+      handler (newItem) {
+        if (!newItem || !newItem.id) {
+          return
+        }
+        this.fetchData()
+      }
+    }
+  },
+  inject: ['parentFetchData'],
+  methods: {
+    fetchData () {
+      const params = {
+        listall: true,
+        page: this.page,
+        pagesize: this.pageSize,
+        userid: this.resource.id
+      }
+      this.fetchLoading = true
+      getAPI('listUserKeys', params).then(json => {
+        this.totalKeypairs = json.listuserkeysresponse.count || 0
+        this.keypairs = json.listuserkeysresponse.userapikey || []
+      }).finally(() => {
+        this.fetchLoading = false
+      })
+    },
+    setSelection (selection) {
+      this.selectedRowKeys = selection
+      this.$emit('selection-change', this.selectedRowKeys)
+      this.selectedItems = (this.keypairs.filter(function (item) {
+        return selection.indexOf(item.id) !== -1
+      }))
+    },
+    changePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    changePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    onShowAddKeyPair () {
+      this.showAddKeyPair = true
+    },
+    eraseKeypairs () {
+      this.selectedColumns.splice(0, 0, {
+        dataIndex: 'status',
+        title: this.$t('label.operation.status'),
+        slots: { customRender: 'status' },
+        filters: [
+          { text: 'In Progress', value: 'InProgress' },
+          { text: 'Success', value: 'success' },
+          { text: 'Failed', value: 'failed' }
+        ]
+      })
+      if (this.selectedRowKeys.length > 0) {
+        this.showGroupActionModal = true
+      }
+      this.deleteKeypairs(this.selectedItems)
+    },
+    async deleteKeypairs (keypairs) {
+      if (!keypairs || keypairs.length === 0) {
+        this.fetchLoading = false
+        return
+      }
+
+      this.fetchLoading = true
+      try {
+        await Promise.all(keypairs.map(async keypair => {
+          try {
+            const jobId = await this.deleteKeyPair({
+              keypairid: keypair.id
+            })
+            await this.$pollJob({
+              jobId,
+              action: {
+                isFetchData: false
+              },
+              successMethod: () => {
+                console.log('success method')

Review Comment:
   Remove the leftover `console.log('success method')` from the bulk-delete 
poll success handler; it will spam browser devtools during normal usage.
   



##########
ui/public/locales/en.json:
##########
@@ -3943,6 +3963,7 @@
 "message.success.create.keypair": "Successfully created SSH key pair",
 "message.success.create.kubernetes.cluster": "Successfully created Kubernetes 
Cluster",
 "message.success.create.l2.network": "Successfully created L2 Network",
+"message.success.create.password": "Successfully created API key pair for 
user",

Review Comment:
   The `message.success.create.password` translation key added here appears 
unrelated to API key pairs and is not referenced anywhere in the UI. If this is 
meant for API key pair creation, rename it to the correct/used key (or remove 
it) to avoid accumulating unused i18n entries.
   



##########
ui/src/views/AutogenView.vue:
##########
@@ -1132,6 +1132,10 @@ export default {
             params.name = this.$route.params.id
           }
         }
+        if (['listUserKeys'].includes(this.apiName)) {
+          delete params.listall

Review Comment:
   In the `listUserKeys` special-case, `params.id` is still set earlier from 
the route param. `listUserKeys` does not define an `id` parameter, so this 
results in an “Unknown parameters: id” warning on the management server logs. 
Remove `params.id` here (or avoid setting it for this API) when mapping the 
route param to `keypairid`.
   



##########
ui/src/components/view/ApiKeyPairsTab.vue:
##########
@@ -0,0 +1,456 @@
+// 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
+        v-if="'registerUserKeys' in $store.getters.apis"
+        type="dashed"
+        style="width: 100%; margin-bottom: 15px"
+        @click="onShowAddKeyPair()">
+        <template #icon><plus-outlined /></template>
+        {{ $t('label.register.api.key') }}
+      </a-button>
+      <a-button
+        v-if="this.selectedRowKeys.length > 0 && ('deleteUserKeys' in 
$store.getters.apis)"
+        type="primary"
+        danger
+        style="width: 100%; margin-bottom: 15px"
+        @click="bulkActionConfirmation()">
+        <template #icon><delete-outlined /></template>
+        {{ $t('label.action.bulk.delete.api.keys') }}
+      </a-button>
+      <a-table
+        size="small"
+        style="overflow-y: auto"
+        :columns="columns"
+        :dataSource="keypairs"
+        :rowKey="item => item.id"
+        :rowSelection="rowSelection()"
+        :pagination="false" >
+        <template #name="{ record }">
+          <div>
+            <router-link :to="{ path: '/keypair/' + record.id }" >
+              {{ record.name }}
+            </router-link>
+          </div>
+        </template>
+        <template #apikey="{ record }">
+          <strong>
+            <tooltip-button
+              tooltipPlacement="right"
+              :tooltip="$t('label.copy')"
+              icon="CopyOutlined"
+              type="dashed"
+              size="small"
+              @onClick="$message.success($t('label.copied.clipboard'))"
+              :copyResource="record.apikey" />
+          </strong>
+          <div>
+            {{ record.apikey.substring(0, 20) }}...
+          </div>
+        </template>
+
+        <template #secretkey="{ record }">
+          <strong>
+            <tooltip-button
+              tooltipPlacement="right"
+              :tooltip="$t('label.copy')"
+              icon="CopyOutlined"
+              type="dashed"
+              size="small"
+              @onClick="$message.success($t('label.copied.clipboard'))"
+              :copyResource="record.secretkey" />
+          </strong>
+          <div>
+            {{ record.secretkey.substring(0, 20) }}...
+          </div>
+        </template>
+
+        <template #startdate="{ record }">
+          <div> {{ $toLocaleDate(record.startdate) }} </div>
+        </template>
+
+        <template #enddate="{ record }">
+          <div> {{ $toLocaleDate(record.enddate)}} </div>
+        </template>
+
+        <template #created="{ record }">
+          <div> {{ $toLocaleDate(record.created) }} </div>
+        </template>
+
+      </a-table>
+      <a-divider/>
+      <a-pagination
+        class="row-element pagination"
+        size="small"
+        :current="page"
+        :pageSize="pageSize"
+        :total="totalKeypairs"
+        :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>
+    <bulk-action-view
+      v-if="(showConfirmationAction || showGroupActionModal)"
+      :showConfirmationAction="showConfirmationAction"
+      :showGroupActionModal="showGroupActionModal"
+      :items="keypairs"
+      :selectedRowKeys="selectedRowKeys"
+      :selectedItems="selectedItems"
+      :columns="columns"
+      :selectedColumns="selectedColumns"
+      action="eraseKeypairs"
+      :loading="loading"
+      :message="bulkDeleteMessage"
+      @group-action="eraseKeypairs"
+      @handle-cancel="handleCancelBulk"
+      @close-modal="closeModalBulk" />
+    <generate-api-key-pair
+      :showAddKeyPair="showAddKeyPair"
+      :resource="resource"
+      @fetch-data="fetchData"
+      @refresh-data="handleRefreshData"
+      @close-modal="closeModalAddKeyPair" />
+  </div>
+</template>
+<script>
+import { getAPI, postAPI } from '@/api'
+import Status from '@/components/widgets/Status'
+import TooltipButton from '@/components/widgets/TooltipButton'
+import BulkActionView from '@/components/view/BulkActionView.vue'
+import eventBus from '@/config/eventBus'
+import OwnershipSelection from '@/views/compute/wizard/OwnershipSelection.vue'
+import GenerateApiKeyPair from '@/views/iam/GenerateApiKeyPair.vue'
+
+export default {
+  name: 'ApiKeyPairsTab',
+  components: {
+    OwnershipSelection,
+    Status,
+    TooltipButton,
+    BulkActionView,
+    GenerateApiKeyPair
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data () {
+    return {
+      fetchLoading: false,
+      keypairs: [],
+      page: 1,
+      pageSize: 10,
+      totalKeypairs: 0,
+      selectedRowKeys: [],
+      selectedItems: [],
+      selectedColumns: [],
+      filterColumns: ['Action'],
+      showConfirmationAction: false,
+      showAddKeyPair: false,
+      showGroupActionModal: false,
+      bulkDeleteMessage: {
+        title: this.$t('label.action.bulk.delete.api.keys'),
+        confirmMessage: this.$t('label.confirm.delete.api.keys')
+      },
+      columns: [
+        {
+          title: this.$t('label.name'),
+          dataIndex: 'name',
+          slots: { customRender: 'name' }
+        },
+        {
+          title: this.$t('label.apikey'),
+          dataIndex: 'apikey',
+          slots: { customRender: 'apikey' }
+        },
+        {
+          title: this.$t('label.secretkey'),
+          dataIndex: 'secretkey',
+          slots: { customRender: 'secretkey' }
+        },
+        {
+          title: this.$t('label.start.date'),
+          dataIndex: 'startdate',
+          slots: { customRender: 'startdate' }
+        },
+        {
+          title: this.$t('label.end.date'),
+          dataIndex: 'enddate',
+          slots: { customRender: 'enddate' }
+        },
+        {
+          title: this.$t('label.created'),
+          dataIndex: 'created',
+          slots: { customRender: 'created' }
+        }
+      ]
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  watch: {
+    resource: {
+      deep: true,
+      handler (newItem) {
+        if (!newItem || !newItem.id) {
+          return
+        }
+        this.fetchData()
+      }
+    }
+  },
+  inject: ['parentFetchData'],
+  methods: {
+    fetchData () {
+      const params = {
+        listall: true,
+        page: this.page,
+        pagesize: this.pageSize,
+        userid: this.resource.id
+      }
+      this.fetchLoading = true
+      getAPI('listUserKeys', params).then(json => {
+        this.totalKeypairs = json.listuserkeysresponse.count || 0
+        this.keypairs = json.listuserkeysresponse.userapikey || []
+      }).finally(() => {
+        this.fetchLoading = false
+      })
+    },
+    setSelection (selection) {
+      this.selectedRowKeys = selection
+      this.$emit('selection-change', this.selectedRowKeys)
+      this.selectedItems = (this.keypairs.filter(function (item) {
+        return selection.indexOf(item.id) !== -1
+      }))
+    },
+    changePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    changePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    onShowAddKeyPair () {
+      this.showAddKeyPair = true
+    },
+    eraseKeypairs () {
+      this.selectedColumns.splice(0, 0, {
+        dataIndex: 'status',
+        title: this.$t('label.operation.status'),
+        slots: { customRender: 'status' },
+        filters: [
+          { text: 'In Progress', value: 'InProgress' },
+          { text: 'Success', value: 'success' },
+          { text: 'Failed', value: 'failed' }
+        ]
+      })

Review Comment:
   `eraseKeypairs()` always prepends the status column to `selectedColumns`. If 
the user re-runs the bulk delete in the same session, this will add duplicate 
status columns. Guard against re-adding the column (e.g., check whether 
`selectedColumns` already contains `dataIndex: 'status'` before splicing).
   



##########
api/src/main/java/org/apache/cloudstack/api/command/admin/user/RegisterUserKeysCmd.java:
##########
@@ -138,6 +138,9 @@ public List<Map<String, Object>> getRules() {
 
             String description = detail.get(ApiConstants.DESCRIPTION);
             if (StringUtils.isNotEmpty(description)) {
+                if (description.length() > 255) {
+                    throw new ServerApiException(ApiErrorCode.PARAM_ERROR, 
"Description cannot be longer than 255 characters.");

Review Comment:
   This error message is ambiguous because the API also has a top-level 
`description` parameter for the key pair. Consider clarifying that this limit 
applies to the rule's description (e.g., “Rule description cannot be longer 
than 255 characters”).
   



##########
ui/src/views/iam/ApiKeyPairPermissionTable.vue:
##########
@@ -0,0 +1,520 @@
+// 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>
+  <loading-outlined v-if="loadingTable" class="main-loading-spinner" />
+  <div v-else>
+    <div v-if="!disabled" class="rules-list ant-list ant-list-bordered">
+      <div class="rules-table-item ant-list-item">
+        <div class="rules-table__col rules-table__col--grab" />
+        <div class="rules-table__col rules-table__col--rule 
rules-table__col--new">
+          <a-auto-complete
+            :key="autocompleteKey"
+            v-focus="true"
+            :filterOption="filterOption"
+            :options="apis"
+            v-model:value="newRule"
+            :placeholder="$t('label.rule')"
+            :class="{'rule-dropdown-error' : newRuleSelectError}" />

Review Comment:
   `newRuleSelectError` is only used for CSS binding and is never updated 
anywhere, so the error styling can never activate. Either implement the error 
state updates (e.g., when the rule is empty/duplicate/invalid) or remove the 
unused state+CSS to keep the component consistent.
   



##########
ui/src/views/iam/GenerateApiKeyPair.vue:
##########
@@ -0,0 +1,229 @@
+// 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-modal
+      v-if="showAddKeyPair"
+      :visible="showAddKeyPair"
+      :closable="true"
+      :maskClosable="false"
+      :okText="$t('label.ok')"
+      :cancelText="$t('label.cancel')"
+      style="top: 20px;"
+      width="50vw"
+      @cancel="closeModal"
+      @ok="handleSubmit"
+      :ok-button-props="{props: { type: 'default' } }"
+      :cancel-button-props="{props: { type: 'primary' } }"
+      centered>
+      <template #title>
+        {{ $t('label.action.create.api.key') }}
+      </template>
+      <a-spin :spinning="loading">
+        <a-form
+          :ref="formRef"
+          :model="form"
+          layout="vertical"
+          @finish="handleSubmit">
+          <a-alert
+            style="margin-bottom: 10px; "
+            :message="$t('message.note.about.keypair.permissions.title')"
+            :description="$t('message.note.about.keypair.permissions.body')"
+            type="info"
+            show-icon
+          />
+          <a-form-item name="name" ref="name">
+            <template #label>
+              <tooltip-label :title="$t('label.name')" 
:tooltip="apiParams.name.description"/>
+            </template>
+            <a-input
+              v-focus="true"
+              :placeholder="$t('label.apikeypair.name')"
+              v-model:value="form.name" />
+          </a-form-item>
+          <a-form-item name="description" ref="description">
+            <template #label>
+              <tooltip-label :title="$t('label.description')" 
:tooltip="apiParams.description.description"/>
+            </template>
+            <a-input
+              v-model:value="form.description"
+              :placeholder="$t('label.apikeypair.description')" />
+          </a-form-item>
+          <a-row>
+            <a-form-item ref="startDate" name="startDate">
+              <template #label>
+                <tooltip-label :title="$t('label.start.date')" 
:tooltip="apiParams.startdate.description"/>
+              </template>
+              <a-date-picker
+                v-model:value="form.startDate"
+                :disabled-date="disabledStartDate"
+                show-time
+              />
+            </a-form-item>
+            <a-form-item ref="endDate" name="endDate" style="margin: 0 8px">
+              <template #label>
+                <tooltip-label :title="$t('label.end.date')" 
:tooltip="apiParams.enddate.description"/>
+              </template>
+              <a-date-picker
+                :disabled-date="disabledEndDate"
+                v-model:value="form.endDate"
+                show-time />
+            </a-form-item>
+          </a-row>
+          <a-form-item>
+            <template #label>
+              <tooltip-label :title="$t('label.rules')" 
:tooltip="apiParams.rules.description"/>
+            </template>
+            <api-key-pair-permission-table
+              :resource="resource"
+              @update-rules="updateRules"/>
+          </a-form-item>
+        </a-form>
+      </a-spin>
+    </a-modal>
+  </div>
+</template>
+
+<script>
+import { ref, reactive, toRaw } from 'vue'
+import { postAPI } from '@/api'
+import TooltipLabel from '@/components/widgets/TooltipLabel'
+import ApiKeyPairPermissionTable from 
'@/views/iam/ApiKeyPairPermissionTable.vue'
+import { dayjs, parseDayJsObject } from '@/utils/date'
+
+export default {
+  name: 'GenerateApiKeyPair',
+  components: {
+    TooltipLabel,
+    ApiKeyPairPermissionTable
+  },
+  props: {
+    showAddKeyPair: {
+      type: Boolean,
+      default: false
+    },
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      rules: [],
+      loading: false
+    }
+  },
+  beforeCreate () {
+    this.apiParams = this.$getApiParams('registerUserKeys')
+  },
+  created () {
+    this.initForm()
+  },
+  methods: {
+    initForm () {
+      this.formRef = ref()
+      this.form = reactive({})
+    },
+    isValidValueForKey (obj, key) {
+      return key in obj && obj[key] != null
+    },

Review Comment:
   `isValidValueForKey` is declared but never used. Please remove it (or use 
it) to avoid dead code in this component.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to