From f47459f436a989d2c65d2423f3b1b7d897a962f7 Mon Sep 17 00:00:00 2001 From: Sheldon Led Date: Fri, 21 Mar 2025 11:29:12 +0000 Subject: [PATCH] Delete streaming destinations from new API --- .../components/audit_events_stream.vue | 7 ++- .../components/stream/stream_delete_modal.vue | 17 +++++ .../stream/stream_destination_editor.vue | 9 ++- .../audit_events/graphql/cache_update.js | 35 ++++++++++- ...oup_streaming_destination.mutation.graphql | 5 ++ ...nce_streaming_destination.mutation.graphql | 5 ++ .../components/audit_events_stream_spec.js | 26 ++++++++ .../stream/stream_delete_modal_spec.js | 63 ++++++++++++++++++- .../stream/stream_destination_editor_spec.js | 51 +++++++++++++++ .../audit_events/graphql/cache_update_spec.js | 14 ++--- ee/spec/frontend/audit_events/mock_data.js | 8 +++ 11 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 ee/app/assets/javascripts/audit_events/graphql/mutations/delete_group_streaming_destination.mutation.graphql create mode 100644 ee/app/assets/javascripts/audit_events/graphql/mutations/delete_instance_streaming_destination.mutation.graphql diff --git a/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue b/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue index dbe8d1416d7ba7..951f35d7dbcbbf 100644 --- a/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue +++ b/ee/app/assets/javascripts/audit_events/components/audit_events_stream.vue @@ -18,6 +18,7 @@ import { } from '../constants'; import { removeAuditEventsStreamingDestination, + removeLegacyAuditEventsStreamingDestination, removeGcpLoggingAuditEventsStreamingDestination, removeAmazonS3AuditEventsStreamingDestination, } from '../graphql/cache_update'; @@ -163,7 +164,11 @@ export default { this.hideEditor(); }, async onDeletedDestination(id) { - removeAuditEventsStreamingDestination({ + const removeFn = this.glFeatures.useConsolidatedAuditEventStreamDestApi + ? removeAuditEventsStreamingDestination + : removeLegacyAuditEventsStreamingDestination; + + removeFn({ store: this.$apollo.provider.defaultClient, fullPath: this.groupPath, destinationId: id, diff --git a/ee/app/assets/javascripts/audit_events/components/stream/stream_delete_modal.vue b/ee/app/assets/javascripts/audit_events/components/stream/stream_delete_modal.vue index 4da6b3f37b6731..603fea84b8ecf0 100644 --- a/ee/app/assets/javascripts/audit_events/components/stream/stream_delete_modal.vue +++ b/ee/app/assets/javascripts/audit_events/components/stream/stream_delete_modal.vue @@ -2,6 +2,10 @@ import { GlModal, GlSprintf } from '@gitlab/ui'; import { __, s__ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import deleteGroupStreamingDestinationsQuery from '../../graphql/mutations/delete_group_streaming_destination.mutation.graphql'; +import deleteInstanceStreamingDestinationsQuery from '../../graphql/mutations/delete_instance_streaming_destination.mutation.graphql'; +// Legacy Mutations 👇 To be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/523881 import deleteExternalDestination from '../../graphql/mutations/delete_external_destination.mutation.graphql'; import deleteInstanceExternalDestination from '../../graphql/mutations/delete_instance_external_destination.mutation.graphql'; import googleCloudLoggingConfigurationDestroy from '../../graphql/mutations/delete_gcp_logging_destination.mutation.graphql'; @@ -20,6 +24,7 @@ export default { GlModal, GlSprintf, }, + mixins: [glFeatureFlagMixin()], inject: ['groupPath'], props: { item: { @@ -36,6 +41,12 @@ export default { return this.groupPath === 'instance'; }, destinationDestroyMutation() { + if (this.glFeatures.useConsolidatedAuditEventStreamDestApi) { + return this.isInstance + ? deleteInstanceStreamingDestinationsQuery + : deleteGroupStreamingDestinationsQuery; + } + switch (this.type) { case DESTINATION_TYPE_GCP_LOGGING: return this.isInstance @@ -56,6 +67,12 @@ export default { }, methods: { destinationErrors(data) { + if (this.glFeatures.useConsolidatedAuditEventStreamDestApi) { + return this.isInstance + ? data.instanceAuditEventStreamingDestinationsDelete.errors + : data.groupAuditEventStreamingDestinationsDelete.errors; + } + switch (this.type) { case DESTINATION_TYPE_GCP_LOGGING: return this.isInstance diff --git a/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue b/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue index 091b50f2edc761..f69eac8889533c 100644 --- a/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue +++ b/ee/app/assets/javascripts/audit_events/components/stream/stream_destination_editor.vue @@ -14,6 +14,7 @@ import { import { isEqual } from 'lodash'; import { GlTooltipDirective as GlTooltip } from '@gitlab/ui/dist/directives/tooltip'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { createAlert } from '~/alert'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import externalAuditEventDestinationCreate from '../../graphql/mutations/create_external_destination.mutation.graphql'; @@ -44,6 +45,7 @@ import { import { addAuditEventsStreamingDestination, removeAuditEventsStreamingDestination, + removeLegacyAuditEventsStreamingDestination, addAuditEventStreamingHeader, removeAuditEventStreamingHeader, updateEventTypeFilters, @@ -83,6 +85,7 @@ export default { directives: { GlTooltip, }, + mixins: [glFeatureFlagMixin()], inject: ['groupPath', 'maxHeaders'], props: { item: { @@ -408,6 +411,9 @@ export default { }, async deleteCreatedDestination(destinationId) { const { groupPath: fullPath, isInstance } = this; + const removeFn = this.glFeatures.useConsolidatedAuditEventStreamDestApi + ? removeAuditEventsStreamingDestination + : removeLegacyAuditEventsStreamingDestination; return this.$apollo.mutate({ mutation: this.destinationVariables.destinationDestroyMutation, variables: { @@ -421,7 +427,7 @@ export default { return; } - removeAuditEventsStreamingDestination({ + removeFn({ store: cache, fullPath, destinationId, @@ -556,7 +562,6 @@ export default { this.errors = []; this.loading = true; - try { const errors = []; const { errors: destinationErrors = [], externalAuditEventDestination } = diff --git a/ee/app/assets/javascripts/audit_events/graphql/cache_update.js b/ee/app/assets/javascripts/audit_events/graphql/cache_update.js index e4bb808730f470..39560fc0c5f9b1 100644 --- a/ee/app/assets/javascripts/audit_events/graphql/cache_update.js +++ b/ee/app/assets/javascripts/audit_events/graphql/cache_update.js @@ -1,4 +1,7 @@ import produce from 'immer'; +import groupStreamingDestinationsQuery from './queries/get_group_streaming_destinations.query.graphql'; +import instanceStreamingDestinationsQuery from './queries/get_instance_streaming_destinations.query.graphql'; +// legacy Queries 👇 to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/523881 import getExternalDestinationsQuery from './queries/get_external_destinations.query.graphql'; import getInstanceExternalDestinationsQuery from './queries/get_instance_external_destinations.query.graphql'; import gcpLoggingDestinationsQuery from './queries/get_google_cloud_logging_destinations.query.graphql'; @@ -11,6 +14,36 @@ import InstanceExternalAuditEventDestinationFragment from './fragments/instance_ const EXTERNAL_AUDIT_EVENT_DESTINATION_TYPENAME = 'ExternalAuditEventDestination'; const INSTANCE_EXTERNAL_AUDIT_EVENT_DESTINATION_TYPENAME = 'InstanceExternalAuditEventDestination'; +export function removeAuditEventsStreamingDestination({ store, fullPath, destinationId }) { + const getDestinationQuery = + fullPath === 'instance' ? instanceStreamingDestinationsQuery : groupStreamingDestinationsQuery; + const sourceData = store.readQuery({ + query: getDestinationQuery, + variables: { fullPath }, + }); + + if (!sourceData) { + return; + } + + const data = produce(sourceData, (draftData) => { + if (fullPath === 'instance') { + draftData.auditEventsInstanceStreamingDestinations.nodes = + draftData.auditEventsInstanceStreamingDestinations.nodes.filter( + (node) => node.id !== destinationId, + ); + } else { + draftData.group.externalAuditEventStreamingDestinations.nodes = + draftData.group.externalAuditEventStreamingDestinations.nodes.filter( + (node) => node.id !== destinationId, + ); + } + }); + store.writeQuery({ query: getDestinationQuery, variables: { fullPath }, data }); +} + +// legacy functions 👇 to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/523881 + function makeDestinationIdRecord(store, id) { return { id: store.identify({ @@ -57,7 +90,7 @@ export function addAuditEventsStreamingDestination({ store, fullPath, newDestina store.writeQuery({ query: getDestinationQuery, variables: { fullPath }, data }); } -export function removeAuditEventsStreamingDestination({ store, fullPath, destinationId }) { +export function removeLegacyAuditEventsStreamingDestination({ store, fullPath, destinationId }) { const getDestinationQuery = fullPath === 'instance' ? getInstanceExternalDestinationsQuery : getExternalDestinationsQuery; const sourceData = store.readQuery({ diff --git a/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_group_streaming_destination.mutation.graphql b/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_group_streaming_destination.mutation.graphql new file mode 100644 index 00000000000000..a4eb0cab0db04f --- /dev/null +++ b/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_group_streaming_destination.mutation.graphql @@ -0,0 +1,5 @@ +mutation deleteGroupDestination($id: AuditEventsGroupExternalStreamingDestinationID!) { + groupAuditEventStreamingDestinationsDelete(input: { id: $id }) { + errors + } +} diff --git a/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_instance_streaming_destination.mutation.graphql b/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_instance_streaming_destination.mutation.graphql new file mode 100644 index 00000000000000..9b897fb95278ce --- /dev/null +++ b/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_instance_streaming_destination.mutation.graphql @@ -0,0 +1,5 @@ +mutation deleteInstanceDestination($id: AuditEventsInstanceExternalStreamingDestinationID!) { + instanceAuditEventStreamingDestinationsDelete(input: { id: $id }) { + errors + } +} diff --git a/ee/spec/frontend/audit_events/components/audit_events_stream_spec.js b/ee/spec/frontend/audit_events/components/audit_events_stream_spec.js index ec9e9186d8e292..ee0b8d79f29ae6 100644 --- a/ee/spec/frontend/audit_events/components/audit_events_stream_spec.js +++ b/ee/spec/frontend/audit_events/components/audit_events_stream_spec.js @@ -389,6 +389,19 @@ describe('AuditEventsStream', () => { new Error('Unknown destination category: something_else'), ); }); + + it('updates list when destination is removed', async () => { + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + expect(streamingDestinationsQuerySpy).toHaveBeenCalledTimes(1); + + const currentLength = findStreamItems().length; + findStreamItems().at(0).vm.$emit('deleted'); + await waitForPromises(); + expect(findStreamItems()).toHaveLength(currentLength - 1); + expect(findSuccessMessage().text()).toBe(DELETE_STREAM_MESSAGE); + }); }); }); }); @@ -655,6 +668,19 @@ describe('AuditEventsStream', () => { expect(streamItem.props('item').id).toBe(mockAllConsolidatedAPIDestinations[index].id); }); }); + + it('updates list when destination is removed', async () => { + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + expect(instanceStreamingDestinationsQuerySpy).toHaveBeenCalledTimes(1); + + const currentLength = findStreamItems().length; + findStreamItems().at(0).vm.$emit('deleted'); + await waitForPromises(); + expect(findStreamItems()).toHaveLength(currentLength - 1); + expect(findSuccessMessage().text()).toBe(DELETE_STREAM_MESSAGE); + }); }); }); }); diff --git a/ee/spec/frontend/audit_events/components/stream/stream_delete_modal_spec.js b/ee/spec/frontend/audit_events/components/stream/stream_delete_modal_spec.js index 1d027f2ad7d02b..ca47906d6ab50c 100644 --- a/ee/spec/frontend/audit_events/components/stream/stream_delete_modal_spec.js +++ b/ee/spec/frontend/audit_events/components/stream/stream_delete_modal_spec.js @@ -4,6 +4,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import StreamDeleteModal from 'ee/audit_events/components/stream/stream_delete_modal.vue'; +import deleteGroupStreamingDestinationsQuery from 'ee/audit_events/graphql/mutations/delete_group_streaming_destination.mutation.graphql'; import deleteExternalDestination from 'ee/audit_events/graphql/mutations/delete_external_destination.mutation.graphql'; import deleteInstanceExternalDestination from 'ee/audit_events/graphql/mutations/delete_instance_external_destination.mutation.graphql'; import googleCloudLoggingConfigurationDestroy from 'ee/audit_events/graphql/mutations/delete_gcp_logging_destination.mutation.graphql'; @@ -14,6 +15,8 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { groupPath, + streamingDestinationDeleteMutationPopulator, + mockConsolidatedAPIExternalDestinations, destinationDeleteMutationPopulator, mockExternalDestinations, mockHttpType, @@ -43,6 +46,16 @@ describe('StreamDeleteModal', () => { const mockAmazonS3Destination = mockAmazonS3Destinations[0]; const instanceAmazonS3Destination = mockInstanceAmazonS3Destinations[0]; + const deleteStreamingSuccess = jest + .fn() + .mockResolvedValue(streamingDestinationDeleteMutationPopulator()); + const deleteStreamingError = jest + .fn() + .mockResolvedValue(streamingDestinationDeleteMutationPopulator(['Random Error message'])); + const deleteStreamingNetworkError = jest + .fn() + .mockRejectedValue(streamingDestinationDeleteMutationPopulator(['Network error'])); + const deleteSuccess = jest.fn().mockResolvedValue(destinationDeleteMutationPopulator()); const deleteInstanceSuccess = jest .fn() @@ -74,7 +87,7 @@ describe('StreamDeleteModal', () => { const findModal = () => wrapper.findComponent(GlModal); const clickDeleteFramework = () => findModal().vm.$emit('primary'); - const createComponent = (resolverMock) => { + const createComponent = (resolverMock, useConsolidatedAuditEventStreamDestApi = false) => { const mockApollo = createMockApollo([[deleteExternalDestinationProvide, resolverMock]]); wrapper = shallowMount(StreamDeleteModal, { @@ -85,6 +98,9 @@ describe('StreamDeleteModal', () => { }, provide: { groupPath: groupPathProvide, + glFeatures: { + useConsolidatedAuditEventStreamDestApi, + }, }, stubs: { GlSprintf, @@ -159,6 +175,51 @@ describe('StreamDeleteModal', () => { expect(wrapper.emitted('error')).toHaveLength(1); }); + describe('with useConsolidatedAuditEventStreamDestApi feature flag', () => { + beforeEach(() => { + deleteExternalDestinationProvide = deleteGroupStreamingDestinationsQuery; + [itemProvide] = mockConsolidatedAPIExternalDestinations; + }); + + it('calls the delete mutation with the destination ID', async () => { + createComponent(deleteStreamingSuccess, true); + clickDeleteFramework(); + + await waitForPromises(); + + expect(deleteStreamingSuccess).toHaveBeenCalledWith({ + id: mockConsolidatedAPIExternalDestinations[0].id, + isInstance: false, + }); + }); + + it('emits "delete" event when the destination is successfully deleted', async () => { + createComponent(deleteStreamingSuccess, true); + clickDeleteFramework(); + + await waitForPromises(); + + expect(wrapper.emitted('delete')).toHaveLength(1); + }); + + it('emits "error" event when there is a network error', async () => { + createComponent(deleteStreamingNetworkError, true); + clickDeleteFramework(); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toHaveLength(1); + }); + + it('emits "error" event when there is a graphql error', async () => { + createComponent(deleteStreamingError, true); + clickDeleteFramework(); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toHaveLength(1); + }); + }); }); describe('Group GCP Logging clickDeleteDestination', () => { diff --git a/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js b/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js index bb50221bb02eec..f7a0fc8f00ea9a 100644 --- a/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js +++ b/ee/spec/frontend/audit_events/components/stream/stream_destination_editor_spec.js @@ -8,6 +8,10 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { + removeAuditEventsStreamingDestination, + removeLegacyAuditEventsStreamingDestination, +} from 'ee/audit_events/graphql/cache_update'; import getNamespaceFiltersQuery from 'ee/audit_events/graphql/queries/get_namespace_filters.query.graphql'; import externalAuditEventDestinationCreate from 'ee/audit_events/graphql/mutations/create_external_destination.mutation.graphql'; import externalAuditEventDestinationUpdate from 'ee/audit_events/graphql/mutations/update_external_destination.mutation.graphql'; @@ -70,6 +74,8 @@ import { } from '../../mock_data'; jest.mock('~/alert'); +jest.mock('ee/audit_events/graphql/cache_update'); + Vue.use(VueApollo); describe('StreamDestinationEditor', () => { @@ -210,6 +216,31 @@ describe('StreamDestinationEditor', () => { }); expect(findDeleteModal().props('item')).toBe(item); }); + + describe('when there is an error on adding a destination header', () => { + it('should call removeAuditEventsStreamingDestination', async () => { + createComponent({ + apolloHandlers: [ + [ + externalAuditEventDestinationCreate, + jest.fn().mockResolvedValue(destinationCreateMutationPopulator()), + ], + [ + externalAuditEventDestinationHeaderCreate, + jest.fn().mockResolvedValue(destinationHeaderCreateMutationPopulator(['error'])), + ], + [deleteExternalDestination, defaultDeleteSpy], + ], + provide: { + glFeatures: { useConsolidatedAuditEventStreamDestApi: true }, + }, + }); + + await submitFormWithHeaders(); + + expect(removeAuditEventsStreamingDestination).toHaveBeenCalled(); + }); + }); }); describe('Group StreamDestinationEditor', () => { @@ -391,6 +422,26 @@ describe('StreamDestinationEditor', () => { expect(wrapper.emitted('added')).toBeUndefined(); }); + it('should call removeLegacyAuditEventsStreamingDestination when server returns error while adding headers', async () => { + createComponent({ + apolloHandlers: [ + [ + externalAuditEventDestinationCreate, + jest.fn().mockResolvedValue(destinationCreateMutationPopulator()), + ], + [ + externalAuditEventDestinationHeaderCreate, + jest.fn().mockResolvedValue(destinationHeaderCreateMutationPopulator(['error'])), + ], + [deleteExternalDestination, defaultDeleteSpy], + ], + }); + + await submitFormWithHeaders(); + + expect(removeLegacyAuditEventsStreamingDestination).toHaveBeenCalled(); + }); + it('should not emit add destination event and reports error when network error occurs while adding headers', async () => { const sentryError = new Error('Network error'); const sentryCaptureExceptionSpy = jest.spyOn(Sentry, 'captureException'); diff --git a/ee/spec/frontend/audit_events/graphql/cache_update_spec.js b/ee/spec/frontend/audit_events/graphql/cache_update_spec.js index 82b39acc2e7f1c..99703a7ad6141a 100644 --- a/ee/spec/frontend/audit_events/graphql/cache_update_spec.js +++ b/ee/spec/frontend/audit_events/graphql/cache_update_spec.js @@ -1,7 +1,7 @@ import { InMemoryCache } from '@apollo/client/core'; import { addAuditEventsStreamingDestination, - removeAuditEventsStreamingDestination, + removeLegacyAuditEventsStreamingDestination, addAuditEventStreamingHeader, removeAuditEventStreamingHeader, updateEventTypeFilters, @@ -210,12 +210,12 @@ describe('Audit events GraphQL cache updates', () => { }); }); - describe('removeAuditEventsStreamingDestination', () => { + describe('removeLegacyAuditEventsStreamingDestination', () => { it('removes new destination to list of destinations for specific fullPath', () => { const [firstDestination, ...restDestinations] = getDestinations(GROUP1_PATH); const { length: originalDestinationsLengthForGroup2 } = getDestinations(GROUP2_PATH); - removeAuditEventsStreamingDestination({ + removeLegacyAuditEventsStreamingDestination({ store: cache, fullPath: GROUP1_PATH, destinationId: firstDestination.id, @@ -230,7 +230,7 @@ describe('Audit events GraphQL cache updates', () => { it('does not throw on non-existing fullPath', () => { expect(() => - removeAuditEventsStreamingDestination({ + removeLegacyAuditEventsStreamingDestination({ store: cache, fullPath: GROUP_NOT_IN_CACHE, destinationId: 'fake-id', @@ -583,12 +583,12 @@ describe('Audit events GraphQL cache updates', () => { }); }); - describe('removeAuditEventsStreamingDestination', () => { + describe('removeLegacyAuditEventsStreamingDestination', () => { it('removes new destination to list of destinations for specific fullPath', () => { const [firstDestination, ...restDestinations] = getInstanceDestinations(); const { length: originalInstanceDestinationsLength } = getInstanceDestinations(); - removeAuditEventsStreamingDestination({ + removeLegacyAuditEventsStreamingDestination({ store: cache, fullPath: 'instance', destinationId: firstDestination.id, @@ -603,7 +603,7 @@ describe('Audit events GraphQL cache updates', () => { it('does not throw on non-existing fullPath', () => { expect(() => - removeAuditEventsStreamingDestination({ + removeLegacyAuditEventsStreamingDestination({ store: cache, fullPath: 'instance', destinationId: 'fake-id', diff --git a/ee/spec/frontend/audit_events/mock_data.js b/ee/spec/frontend/audit_events/mock_data.js index a7a89c9e5176ac..f0511b67797cb1 100644 --- a/ee/spec/frontend/audit_events/mock_data.js +++ b/ee/spec/frontend/audit_events/mock_data.js @@ -669,6 +669,14 @@ export const instanceAmazonS3DestinationUpdateMutationPopulator = (errors = []) }; }; +export const streamingDestinationDeleteMutationPopulator = (errors = []) => ({ + data: { + groupAuditEventStreamingDestinationsDelete: { + errors, + }, + }, +}); + export const destinationDeleteMutationPopulator = (errors = []) => ({ data: { externalAuditEventDestinationDestroy: { -- GitLab