From 058e7216a3f7163f33c9c40b707199f3e6ebf7f7 Mon Sep 17 00:00:00 2001 From: Anna Vovchenko <avovchenko@gitlab.com> Date: Wed, 24 Jul 2024 19:10:29 +0000 Subject: [PATCH] Add delete pod functionality Add mutation, actions dropdown to the pods table, confirmation modal, error alert and success toast. Changelog: added --- .../kubernetes/delete_pod_modal.vue | 9 +++- .../kubernetes/kubernetes_overview.vue | 6 ++- .../components/kubernetes/kubernetes_pods.vue | 3 +- .../kubernetes/kubernetes_status_bar.vue | 1 - .../components/workload_details.vue | 24 +++++++++- locale/gitlab.pot | 8 ++-- .../kubernetes/kubernetes_overview_spec.js | 10 ++++ .../kubernetes/kubernetes_pods_spec.js | 3 +- .../graphql/resolvers/kubernetes_spec.js | 4 +- .../components/workload_details_spec.js | 46 ++++++++++++++++++- .../components/workload_table_spec.js | 2 +- 11 files changed, 99 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/delete_pod_modal.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/delete_pod_modal.vue index 35a97b120cf10..f758eb835f75d 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/delete_pod_modal.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/delete_pod_modal.vue @@ -76,7 +76,7 @@ export default { 'Environments|Are you sure you want to delete %{podName}? This action cannot be undone.', ), buttonPrimary: s__('Environments|Delete pod'), - buttonCancel: s__('Environments|Cancel'), + buttonCancel: __('Cancel'), success: s__('Environments|Pod deleted successfully'), error: __('Error: '), }, @@ -84,7 +84,12 @@ export default { }; </script> <template> - <gl-modal v-model="visible" :modal-id="$options.DELETE_POD_MODAL_ID" @hide="hideModal"> + <gl-modal + v-model="visible" + :modal-id="$options.DELETE_POD_MODAL_ID" + :aria-label="$options.i18n.buttonPrimary" + @hide="hideModal" + > <template #modal-title> <gl-sprintf :message="$options.i18n.title"> <template #podName> diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue index 5b1dde88e9449..22576ef69f190 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_overview.vue @@ -106,6 +106,7 @@ export default { fluxApiError: '', selectedItem: {}, showDetailsDrawer: false, + focusedElement: null, podToDelete: {}, }; }, @@ -173,6 +174,7 @@ export default { openDetailsDrawer(item) { this.selectedItem = item; this.showDetailsDrawer = true; + this.focusedElement = document.activeElement; this.$nextTick(() => { this.$refs.drawer?.$el?.querySelector('button')?.focus(); }); @@ -181,7 +183,7 @@ export default { this.showDetailsDrawer = false; this.selectedItem = {}; this.$nextTick(() => { - this.$refs.status_bar?.$refs?.flux_status_badge?.$el?.focus(); + this.focusedElement?.focus(); }); }, onDeletePod(pod) { @@ -261,7 +263,7 @@ export default { </h2> </template> <template #default> - <workload-details v-if="hasSelectedItem" :item="selectedItem" /> + <workload-details v-if="hasSelectedItem" :item="selectedItem" @delete-pod="onDeletePod" /> </template> </gl-drawer> </div> diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue index 0ec1ad6a57f96..9f484365a7945 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_pods.vue @@ -48,8 +48,9 @@ export default { actions: [ { name: 'delete-pod', - text: s__('KubernetesDashboard|Delete Pod'), + text: s__('KubernetesDashboard|Delete pod'), icon: 'remove', + variant: 'danger', class: '!gl-text-red-500', }, ], diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue index 5977c02d6f471..00512f635e3d7 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue @@ -227,7 +227,6 @@ export default { <template v-else> <gl-badge :id="fluxBadgeId" - ref="flux_status_badge" :icon="syncStatusBadge.icon" :variant="syncStatusBadge.variant" data-testid="sync-badge" diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue index cba723e806e4f..14c1555bad3c6 100644 --- a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue +++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue @@ -1,5 +1,5 @@ <script> -import { GlBadge, GlTruncate } from '@gitlab/ui'; +import { GlBadge, GlTruncate, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { stringify } from 'yaml'; import { s__ } from '~/locale'; import PodLogsButton from '~/environments/environment_details/components/kubernetes/pod_logs_button.vue'; @@ -10,9 +10,13 @@ export default { components: { GlBadge, GlTruncate, + GlButton, WorkloadDetailsItem, PodLogsButton, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { item: { type: Object, @@ -44,6 +48,9 @@ export default { hasContainers() { return Boolean(this.item.containers); }, + hasActions() { + return Boolean(this.item.actions?.length); + }, }, methods: { getLabelBadgeText([key, value]) { @@ -65,6 +72,7 @@ export default { }, i18n: { name: s__('KubernetesDashboard|Name'), + actions: s__('KubernetesDashboard|Actions'), kind: s__('KubernetesDashboard|Kind'), labels: s__('KubernetesDashboard|Labels'), status: s__('KubernetesDashboard|Status'), @@ -82,6 +90,20 @@ export default { <workload-details-item :label="$options.i18n.name"> <span class="gl-break-anywhere"> {{ item.name }}</span> </workload-details-item> + <workload-details-item v-if="hasActions" :label="$options.i18n.actions"> + <span v-for="action of item.actions" :key="action.name"> + <gl-button + v-gl-tooltip + :title="action.text" + :aria-label="action.text" + :variant="action.variant" + :icon="action.icon" + category="secondary" + class="gl-mr-3" + @click="$emit(action.name, item)" + /> + </span> + </workload-details-item> <workload-details-item :label="$options.i18n.kind"> {{ item.kind }} </workload-details-item> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bc0dec17a5694..ff9d65da9f70f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20480,9 +20480,6 @@ msgstr "" msgid "Environments|Auto stops %{autoStopAt}" msgstr "" -msgid "Environments|Cancel" -msgstr "" - msgid "Environments|Clean up" msgstr "" @@ -30320,6 +30317,9 @@ msgstr "" msgid "Kubernetes deployment not found" msgstr "" +msgid "KubernetesDashboard|Actions" +msgstr "" + msgid "KubernetesDashboard|Age" msgstr "" @@ -30353,7 +30353,7 @@ msgstr "" msgid "KubernetesDashboard|Dashboard" msgstr "" -msgid "KubernetesDashboard|Delete Pod" +msgid "KubernetesDashboard|Delete pod" msgstr "" msgid "KubernetesDashboard|Deployment" diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js index 44d7fc4f6d66d..c7560501a629b 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js @@ -377,6 +377,16 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov expect(findDeletePodModal().props('pod')).toEqual(podToDelete); }); + + it('provides correct pod when emitted from the details drawer', async () => { + const podToDelete = mockPodsTableItems[1]; + findKubernetesTabs().vm.$emit('show-resource-details', mockPodsTableItems[1]); + await nextTick(); + findWorkloadDetails().vm.$emit('delete-pod', podToDelete); + await nextTick(); + + expect(findDeletePodModal().props('pod')).toEqual(podToDelete); + }); }); describe('on child component error', () => { diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js index fd5f26f96eaa6..23ac293351b89 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js @@ -106,8 +106,9 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po const actions = [ { name: 'delete-pod', - text: 'Delete Pod', + text: 'Delete pod', icon: 'remove', + variant: 'danger', class: '!gl-text-red-500', }, ]; diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js index a3e8367263760..59d82f4132c12 100644 --- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js +++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js @@ -374,9 +374,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { }); describe('deleteKubernetesPod', () => { - const mockPodsDeleteFn = jest.fn().mockImplementation(() => { - return Promise.resolve({ errors: [] }); - }); + const mockPodsDeleteFn = jest.fn().mockResolvedValue({ errors: [] }); const podToDelete = 'my-pod'; it('should request delete pod API from the cluster_client library', async () => { diff --git a/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js index 2447bf0f5d594..cbc9483ffac40 100644 --- a/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js +++ b/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlBadge, GlTruncate } from '@gitlab/ui'; +import { GlBadge, GlTruncate, GlButton } from '@gitlab/ui'; import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue'; import WorkloadDetailsItem from '~/kubernetes_dashboard/components/workload_details_item.vue'; import { WORKLOAD_STATUS_BADGE_VARIANTS } from '~/kubernetes_dashboard/constants'; @@ -25,6 +25,8 @@ const findAllBadges = () => wrapper.findAllComponents(GlBadge); const findBadge = (at) => findAllBadges().at(at); const findAllPodLogsButtons = () => wrapper.findAllComponents(PodLogsButton); const findPodLogsButton = (at) => findAllPodLogsButtons().at(at); +const findAllButtons = () => wrapper.findAllComponents(GlButton); +const findButton = (at) => findAllButtons().at(at); describe('Workload details component', () => { describe('when minimal fields are provided', () => { @@ -74,6 +76,48 @@ describe('Workload details component', () => { expect(findWorkloadDetailsItem(index).props('collapsible')).toBe(true); }); + describe('when actions are provided', () => { + const actions = [ + { + name: 'delete-pod', + text: 'Delete pod', + icon: 'remove', + variant: 'danger', + }, + ]; + const mockTableItemsWithActions = { + ...mockPodsTableItems[0], + actions, + }; + + beforeEach(() => { + createWrapper(mockTableItemsWithActions); + }); + + it('renders a non-collapsible list item for containers', () => { + expect(findWorkloadDetailsItem(1).props('label')).toBe('Actions'); + expect(findWorkloadDetailsItem(1).props('collapsible')).toBe(false); + }); + + it('renders a button for each action', () => { + expect(findAllButtons()).toHaveLength(1); + }); + + it.each(actions)('renders a button with the correct props', (action) => { + const currentIndex = actions.indexOf(action); + + expect(findButton(currentIndex).props()).toMatchObject({ + variant: action.variant, + icon: action.icon, + }); + + expect(findButton(currentIndex).attributes()).toMatchObject({ + title: action.text, + 'aria-label': action.text, + }); + }); + }); + describe('when containers are provided', () => { const mockTableItemsWithContainers = { ...mockPodsTableItems[0], diff --git a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js index 8866b550a911e..4ac6035be126e 100644 --- a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js +++ b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js @@ -157,7 +157,7 @@ describe('Workload table component', () => { }); it('renders correct props for each dropdown', () => { - expect(findAllActionsDropdowns().at(0).attributes('title')).toEqual('Actions'); + expect(findAllActionsDropdowns().at(0).attributes('title')).toBe('Actions'); expect(findAllActionsDropdowns().at(0).props('items')).toMatchObject([ { text: 'Delete Pod', -- GitLab