n__('AuditStreams|%d destination', 'AuditStreams|%d destinations', count);
export const ADD_STREAM = s__('AuditStreams|Add streaming destination');
+export const ADD_HTTP = s__('AuditStreams|HTTP endpoint');
+export const ADD_GCP_LOGGING = s__('AuditStreams|Google Cloud Logging');
export const ADD_STREAM_MESSAGE = s__('AuditStreams|Stream added successfully');
export const UPDATE_STREAM_MESSAGE = s__('AuditStreams|Stream updated successfully');
export const DELETE_STREAM_MESSAGE = s__('AuditStreams|Stream deleted successfully');
@@ -117,6 +119,15 @@ export const ADD_STREAM_EDITOR_I18N = {
DELETE_BUTTON_TEXT: s__('AuditStreams|Delete destination'),
HEADER_FILTERING: s__('AuditStreams|Event filtering (optional)'),
FILTER_BY_AUDIT_EVENT_TYPE: s__('AuditStreams|Filter by audit event type'),
+ GCP_LOGGING_DESTINATION_PROJECT_ID_LABEL: s__('AuditStreams|Project ID'),
+ GCP_LOGGING_DESTINATION_PROJECT_ID_PLACEHOLDER: s__('AuditStreams|my-google-project'),
+ GCP_LOGGING_DESTINATION_CLIENT_EMAIL_LABEL: s__('AuditStreams|Client Email'),
+ GCP_LOGGING_DESTINATION_CLIENT_EMAIL_PLACEHOLDER: s__(
+ 'AuditStreams|my-email@my-google-project.iam.gservice.account.com',
+ ),
+ GCP_LOGGING_DESTINATION_LOG_ID_LABEL: s__('AuditStreams|Log ID'),
+ GCP_LOGGING_DESTINATION_LOG_ID_PLACEHOLDER: s__('AuditStreams|audit-events'),
+ GCP_LOGGING_DESTINATION_PASSWORD_LABEL: s__('AuditStreams|Private key'),
};
export const AUDIT_STREAMS_EMPTY_STATE_I18N = {
@@ -157,3 +168,6 @@ export const createBlankHeader = () => ({
disabled: false,
validationErrors: { name: '' },
});
+
+export const DESTINATION_TYPE_HTTP = 'http';
+export const DESTINATION_TYPE_GCP_LOGGING = 'gcpLogging';
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 e34dead27810065d88582edd88fc9befdf6808ad..cca5c96b3ecf4f337521fea00fbf4a36ba93b968 100644
--- a/ee/app/assets/javascripts/audit_events/graphql/cache_update.js
+++ b/ee/app/assets/javascripts/audit_events/graphql/cache_update.js
@@ -1,6 +1,7 @@
import produce from 'immer';
import getExternalDestinationsQuery from './queries/get_external_destinations.query.graphql';
import getInstanceExternalDestinationsQuery from './queries/get_instance_external_destinations.query.graphql';
+import gcpLoggingDestinationsQuery from './queries/get_get_google_cloud_logging_destinations.query.graphql';
import ExternalAuditEventDestinationFragment from './fragments/external_audit_event_destination.fragment.graphql';
import InstanceExternalAuditEventDestinationFragment from './fragments/instance_external_audit_event_destination.fragment.graphql';
@@ -143,3 +144,44 @@ export function removeEventTypeFilters({ store, destinationId, filtersToRemove =
});
store.writeFragment({ ...destinationIdRecord, data: destination });
}
+
+export function addGcpLoggingAuditEventsStreamingDestination({ store, fullPath, newDestination }) {
+ const sourceData = store.readQuery({
+ query: gcpLoggingDestinationsQuery,
+ variables: { fullPath },
+ });
+
+ if (!sourceData) {
+ return;
+ }
+
+ const data = produce(sourceData, (draftData) => {
+ const { nodes } = draftData.group.googleCloudLoggingConfigurations;
+ nodes.push(newDestination);
+ });
+
+ store.writeQuery({ query: gcpLoggingDestinationsQuery, variables: { fullPath }, data });
+}
+
+export function removeGcpLoggingAuditEventsStreamingDestination({
+ store,
+ fullPath,
+ destinationId,
+}) {
+ const sourceData = store.readQuery({
+ query: gcpLoggingDestinationsQuery,
+ variables: { fullPath },
+ });
+
+ if (!sourceData) {
+ return;
+ }
+
+ const data = produce(sourceData, (draftData) => {
+ draftData.group.googleCloudLoggingConfigurations.nodes = draftData.group.googleCloudLoggingConfigurations.nodes.filter(
+ (node) => node.id !== destinationId,
+ );
+ });
+
+ store.writeQuery({ query: gcpLoggingDestinationsQuery, variables: { fullPath }, data });
+}
diff --git a/ee/app/assets/javascripts/audit_events/graphql/mutations/create_gcp_logging_destination.mutation.graphql b/ee/app/assets/javascripts/audit_events/graphql/mutations/create_gcp_logging_destination.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..306660a6e00d2b44ccb0c4222a80a00adec6508c
--- /dev/null
+++ b/ee/app/assets/javascripts/audit_events/graphql/mutations/create_gcp_logging_destination.mutation.graphql
@@ -0,0 +1,26 @@
+mutation createGcpLoggingDestination(
+ $fullPath: ID!
+ $googleProjectIdName: String!
+ $clientEmail: String!
+ $privateKey: String!
+ $logIdName: String!
+) {
+ googleCloudLoggingConfigurationCreate(
+ input: {
+ groupPath: $fullPath
+ googleProjectIdName: $googleProjectIdName
+ clientEmail: $clientEmail
+ privateKey: $privateKey
+ logIdName: $logIdName
+ }
+ ) {
+ errors
+ googleCloudLoggingConfiguration {
+ id
+ logIdName
+ privateKey
+ googleProjectIdName
+ clientEmail
+ }
+ }
+}
diff --git a/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_gcp_logging_destination.mutation.graphql b/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_gcp_logging_destination.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..d1b7a3fea44ae1b301d239c4e24b3cbf61ca98c6
--- /dev/null
+++ b/ee/app/assets/javascripts/audit_events/graphql/mutations/delete_gcp_logging_destination.mutation.graphql
@@ -0,0 +1,5 @@
+mutation deleteGcpLoggingDestination($id: AuditEventsGoogleCloudLoggingConfigurationID!) {
+ googleCloudLoggingConfigurationDestroy(input: { id: $id }) {
+ errors
+ }
+}
diff --git a/ee/app/assets/javascripts/audit_events/graphql/mutations/update_gcp_logging_destination.mutation.graphql b/ee/app/assets/javascripts/audit_events/graphql/mutations/update_gcp_logging_destination.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..dfa2bebebf35ccbf772b7d4a0657b5102a354299
--- /dev/null
+++ b/ee/app/assets/javascripts/audit_events/graphql/mutations/update_gcp_logging_destination.mutation.graphql
@@ -0,0 +1,26 @@
+mutation updateGcpLoggingDestination(
+ $id: AuditEventsGoogleCloudLoggingConfigurationID!
+ $googleProjectIdName: String!
+ $clientEmail: String!
+ $privateKey: String!
+ $logIdName: String!
+) {
+ googleCloudLoggingConfigurationUpdate(
+ input: {
+ id: $id
+ googleProjectIdName: $googleProjectIdName
+ clientEmail: $clientEmail
+ privateKey: $privateKey
+ logIdName: $logIdName
+ }
+ ) {
+ errors
+ googleCloudLoggingConfiguration {
+ id
+ logIdName
+ privateKey
+ googleProjectIdName
+ clientEmail
+ }
+ }
+}
diff --git a/ee/app/assets/javascripts/audit_events/graphql/queries/get_get_google_cloud_logging_destinations.query.graphql b/ee/app/assets/javascripts/audit_events/graphql/queries/get_get_google_cloud_logging_destinations.query.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..edcc47f44ca83e3b55a7ecbee4e5f9b0c26cc54d
--- /dev/null
+++ b/ee/app/assets/javascripts/audit_events/graphql/queries/get_get_google_cloud_logging_destinations.query.graphql
@@ -0,0 +1,14 @@
+query getGoogleCloudLoggingDestinations($fullPath: ID!) {
+ group(fullPath: $fullPath) {
+ id
+ googleCloudLoggingConfigurations {
+ nodes {
+ id
+ logIdName
+ privateKey
+ googleProjectIdName
+ clientEmail
+ }
+ }
+ }
+}
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 36f95f17178ca5dda8280827693eee13546f35a1..76ad45795b551d5b78bfa24535480b84974ab13f 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
@@ -1,8 +1,8 @@
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlDisclosureDropdown, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import externalDestinationsQuery from 'ee/audit_events/graphql/queries/get_external_destinations.query.graphql';
@@ -14,6 +14,7 @@ import {
} from 'ee/audit_events/constants';
import AuditEventsStream from 'ee/audit_events/components/audit_events_stream.vue';
import StreamDestinationEditor from 'ee/audit_events/components/stream/stream_destination_editor.vue';
+import StreamGcpLoggingDestinationEditor from 'ee/audit_events/components/stream/stream_gcp_logging_destination_editor.vue';
import StreamItem from 'ee/audit_events/components/stream/stream_item.vue';
import StreamEmptyState from 'ee/audit_events/components/stream/stream_empty_state.vue';
import {
@@ -37,18 +38,32 @@ describe('AuditEventsStream', () => {
.mockResolvedValue(destinationDataPopulator(mockExternalDestinations));
const createComponent = (mockApollo) => {
- wrapper = shallowMountExtended(AuditEventsStream, {
+ wrapper = mountExtended(AuditEventsStream, {
provide: {
groupPath: providedGroupPath,
},
apolloProvider: mockApollo,
+ stubs: {
+ GlAlert: true,
+ GlLoadingIcon: true,
+ StreamItem: true,
+ StreamDestinationEditor: true,
+ StreamGcpLoggingDestinationEditor: true,
+ StreamEmptyState: true,
+ },
});
};
const findSuccessMessage = () => wrapper.findComponent(GlAlert);
- const findAddDestinationButton = () => wrapper.findComponent(GlButton);
+ const findAddDestinationButton = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDisclosureDropdownItem = (index) =>
+ wrapper.findAllComponents(GlDisclosureDropdownItem).at(index).find('button');
+ const findHttpDropdownItem = () => findDisclosureDropdownItem(0);
+ const findGcpLoggingDropdownItem = () => findDisclosureDropdownItem(1);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findStreamDestinationEditor = () => wrapper.findComponent(StreamDestinationEditor);
+ const findStreamGcpLoggingDestinationEditor = () =>
+ wrapper.findComponent(StreamGcpLoggingDestinationEditor);
const findStreamEmptyState = () => wrapper.findComponent(StreamEmptyState);
const findStreamItems = () => wrapper.findAllComponents(StreamItem);
@@ -100,22 +115,22 @@ describe('AuditEventsStream', () => {
return waitForPromises();
});
- it('shows destination editor', async () => {
+ it('shows http destination editor', async () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findStreamDestinationEditor().exists()).toBe(false);
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
+ expect(findAddDestinationButton().props('toggleText')).toBe('Add streaming destination');
+
+ await findHttpDropdownItem().trigger('click');
expect(findStreamDestinationEditor().exists()).toBe(true);
});
- it('exits edit mode when an external destination is added', async () => {
+ it('exits edit mode when an external http destination is added', async () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findStreamDestinationEditor().exists()).toBe(false);
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
+ await findHttpDropdownItem().trigger('click');
const streamDestinationEditorComponent = findStreamDestinationEditor();
@@ -127,17 +142,40 @@ describe('AuditEventsStream', () => {
expect(findSuccessMessage().text()).toBe(ADD_STREAM_MESSAGE);
});
+ it('shows http gcp logging editor', async () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findStreamGcpLoggingDestinationEditor().exists()).toBe(false);
+
+ expect(findAddDestinationButton().props('toggleText')).toBe('Add streaming destination');
+
+ await findGcpLoggingDropdownItem().trigger('click');
+
+ expect(findStreamGcpLoggingDestinationEditor().exists()).toBe(true);
+ });
+
+ it('exits edit mode when an external gcp logging destination is added', async () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findStreamGcpLoggingDestinationEditor().exists()).toBe(false);
+
+ await findGcpLoggingDropdownItem().trigger('click');
+
+ expect(findStreamGcpLoggingDestinationEditor().exists()).toBe(true);
+
+ findStreamGcpLoggingDestinationEditor().vm.$emit('added');
+ await waitForPromises();
+
+ expect(findSuccessMessage().text()).toBe(ADD_STREAM_MESSAGE);
+ });
+
it('clears the success message if an error occurs afterwards', async () => {
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
+ await findHttpDropdownItem().trigger('click');
findStreamDestinationEditor().vm.$emit('added');
await waitForPromises();
expect(findSuccessMessage().text()).toBe(ADD_STREAM_MESSAGE);
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
+ await findHttpDropdownItem().trigger('click');
findStreamDestinationEditor().vm.$emit('error');
await waitForPromises();
@@ -193,16 +231,6 @@ describe('AuditEventsStream', () => {
});
describe('when initialized', () => {
- it('should render the loading icon while waiting for data to be returned', () => {
- const instanceDestinationQuerySpy = jest.fn();
- const mockApollo = createMockApollo([
- [instanceExternalDestinationsQuery, instanceDestinationQuerySpy],
- ]);
- createComponent(mockApollo);
-
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
it('should render empty state when no data is returned', async () => {
const instanceDestinationQuerySpy = jest
.fn()
@@ -246,8 +274,7 @@ describe('AuditEventsStream', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findStreamDestinationEditor().exists()).toBe(false);
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
+ await findHttpDropdownItem().trigger('click');
expect(findStreamDestinationEditor().exists()).toBe(true);
});
@@ -256,30 +283,25 @@ describe('AuditEventsStream', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findStreamDestinationEditor().exists()).toBe(false);
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
-
- const streamDestinationEditorComponent = findStreamDestinationEditor();
+ await findHttpDropdownItem().trigger('click');
- expect(streamDestinationEditorComponent.exists()).toBe(true);
+ expect(findStreamDestinationEditor().exists()).toBe(true);
- streamDestinationEditorComponent.vm.$emit('added');
+ findStreamDestinationEditor().vm.$emit('added');
await waitForPromises();
expect(findSuccessMessage().text()).toBe(ADD_STREAM_MESSAGE);
});
it('clears the success message if an error occurs afterwards', async () => {
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
+ await findHttpDropdownItem().trigger('click');
findStreamDestinationEditor().vm.$emit('added');
await waitForPromises();
expect(findSuccessMessage().text()).toBe(ADD_STREAM_MESSAGE);
- findAddDestinationButton().vm.$emit('click');
- await nextTick();
+ await findHttpDropdownItem().trigger('click');
findStreamDestinationEditor().vm.$emit('error');
await waitForPromises();
diff --git a/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_empty_state_spec.js.snap b/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_empty_state_spec.js.snap
index 8f1a93c4cb8957ea26c03b29c0d91f521d7bf09d..4e2753317dfc828b3571711f2fbf68b29191f76c 100644
--- a/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_empty_state_spec.js.snap
+++ b/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_empty_state_spec.js.snap
@@ -50,17 +50,106 @@ exports[`StreamEmptyState should render correctly 1`] = `
-
-
- Add streaming destination
-
-
+
+
+
+
+
+
+
+
+ Add streaming destination
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ HTTP endpoint
+
+
+
+
+
+
+
+
+ Google Cloud Logging
+
+
+
+
+
+
+
+
+
diff --git a/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap b/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap
index 9654720cdf264ca8565081f0915891a23b05c26f..f25f4d8464cf2ee96634f890de21c178973226c8 100644
--- a/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap
+++ b/ee/spec/frontend/audit_events/components/stream/__snapshots__/stream_item_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`StreamItem Group StreamItem when an item has event filters renders a popover 1`] = `
+exports[`StreamItem Group http StreamItem when an item has event filters renders a popover 1`] = `
{
const deleteInstanceSuccess = jest
.fn()
.mockResolvedValue(destinationInstanceDeleteMutationPopulator());
+ const deleteGcpLoggingSuccess = jest
+ .fn()
+ .mockResolvedValue(destinationGcpLoggingDeleteMutationPopulator());
const deleteError = jest
.fn()
.mockResolvedValue(destinationDeleteMutationPopulator(['Random Error message']));
@@ -36,6 +43,7 @@ describe('StreamDeleteModal', () => {
let groupPathProvide = groupPath;
let itemProvide = mockExternalDestinations[0];
+ let typeProvide = mockHttpType;
let deleteExternalDestinationProvide = deleteExternalDestination;
const findModal = () => wrapper.findComponent(GlModal);
@@ -48,6 +56,7 @@ describe('StreamDeleteModal', () => {
apolloProvider: mockApollo,
propsData: {
item: itemProvide,
+ type: typeProvide,
},
provide: {
groupPath: groupPathProvide,
@@ -79,7 +88,7 @@ describe('StreamDeleteModal', () => {
});
});
- describe('Group clickDeleteDestination', () => {
+ describe('Group HTTP clickDeleteDestination', () => {
it('emits "deleting" event when busy deleting', () => {
createComponent();
clickDeleteFramework();
@@ -127,10 +136,64 @@ describe('StreamDeleteModal', () => {
});
});
+ describe('Group GCP Logging clickDeleteDestination', () => {
+ beforeEach(() => {
+ typeProvide = mockGcpLoggingType;
+ deleteExternalDestinationProvide = googleCloudLoggingConfigurationDestroy;
+ });
+
+ it('emits "deleting" event when busy deleting', () => {
+ createComponent();
+ clickDeleteFramework();
+
+ expect(wrapper.emitted('deleting')).toHaveLength(1);
+ });
+
+ it('calls the delete mutation with the destination ID', async () => {
+ createComponent(deleteGcpLoggingSuccess);
+ clickDeleteFramework();
+
+ await waitForPromises();
+
+ expect(deleteGcpLoggingSuccess).toHaveBeenCalledWith({
+ id: mockExternalDestinations[0].id,
+ isInstance: false,
+ });
+ });
+
+ it('emits "delete" event when the destination is successfully deleted', async () => {
+ createComponent(deleteGcpLoggingSuccess);
+ clickDeleteFramework();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ });
+
+ it('emits "error" event when there is a network error', async () => {
+ createComponent(deleteNetworkError);
+ clickDeleteFramework();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+
+ it('emits "error" event when there is a graphql error', async () => {
+ createComponent(deleteError);
+ clickDeleteFramework();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
describe('Instance clickDeleteDestination', () => {
beforeEach(() => {
groupPathProvide = instanceGroupPath;
itemProvide = instanceDestination;
+ typeProvide = instanceGroupPath;
deleteExternalDestinationProvide = deleteInstanceExternalDestination;
});
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 4081775debd173b5410760bcdff63a684eb5aad4..ef5da2c6e4ca6b73bc5fccf82cd01ca489a7fd0e 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
@@ -231,7 +231,7 @@ describe('StreamDestinationEditor', () => {
await waitForPromises();
expect(findAlertErrors()).toHaveLength(1);
- expect(findAlertErrors().at(0).text()).toBe(errorMsg);
+ expect(findAlertErrors().at(0).text()).toBe(AUDIT_STREAMS_NETWORK_ERRORS.CREATING_ERROR);
expect(wrapper.emitted('error')).toBeDefined();
expect(wrapper.emitted('added')).toBeUndefined();
});
diff --git a/ee/spec/frontend/audit_events/components/stream/stream_empty_state_spec.js b/ee/spec/frontend/audit_events/components/stream/stream_empty_state_spec.js
index f2ce97b29377b803e699dcf310e55cffb6267a82..6183f0eb7409e5c29e6dea66ddc6e48e57883b4a 100644
--- a/ee/spec/frontend/audit_events/components/stream/stream_empty_state_spec.js
+++ b/ee/spec/frontend/audit_events/components/stream/stream_empty_state_spec.js
@@ -1,15 +1,16 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlEmptyState } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import StreamEmptyState from 'ee/audit_events/components/stream/stream_empty_state.vue';
-import { mockSvgPath } from '../../mock_data';
+import { mockSvgPath, groupPath } from '../../mock_data';
describe('StreamEmptyState', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMount(StreamEmptyState, {
+ wrapper = mountExtended(StreamEmptyState, {
provide: {
emptyStateSvgPath: mockSvgPath,
+ groupPath,
},
stubs: {
GlEmptyState,
@@ -17,6 +18,12 @@ describe('StreamEmptyState', () => {
});
};
+ const findAddDestinationButton = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDisclosureDropdownItem = (index) =>
+ wrapper.findAllComponents(GlDisclosureDropdownItem).at(index).find('button');
+ const findHttpDropdownItem = () => findDisclosureDropdownItem(0);
+ const findGcpLoggingDropdownItem = () => findDisclosureDropdownItem(1);
+
beforeEach(() => {
createComponent();
});
@@ -25,9 +32,18 @@ describe('StreamEmptyState', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('should emit add event', () => {
- wrapper.findComponent(GlButton).vm.$emit('click');
+ it('should show options', () => {
+ expect(findAddDestinationButton().exists()).toBe(true);
+ expect(findAddDestinationButton().props('toggleText')).toBe('Add streaming destination');
+ });
+
+ it('emits event on select http', async () => {
+ await findHttpDropdownItem().trigger('click');
+ expect(wrapper.emitted('add')).toStrictEqual([['http']]);
+ });
- expect(wrapper.emitted('add')).toBeDefined();
+ it('emits event on select gcp logging', async () => {
+ await findGcpLoggingDropdownItem().trigger('click');
+ expect(wrapper.emitted('add')).toStrictEqual([['gcpLogging']]);
});
});
diff --git a/ee/spec/frontend/audit_events/components/stream/stream_gcp_logging_destination_editor_spec.js b/ee/spec/frontend/audit_events/components/stream/stream_gcp_logging_destination_editor_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7a8519fd2c65b9f4bad886e73240ffdc991637f
--- /dev/null
+++ b/ee/spec/frontend/audit_events/components/stream/stream_gcp_logging_destination_editor_spec.js
@@ -0,0 +1,325 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlForm } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { createAlert } from '~/alert';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import googleCloudLoggingConfigurationCreate from 'ee/audit_events/graphql/mutations/create_gcp_logging_destination.mutation.graphql';
+import googleCloudLoggingConfigurationUpdate from 'ee/audit_events/graphql/mutations/update_gcp_logging_destination.mutation.graphql';
+import StreamGcpLoggingDestinationEditor from 'ee/audit_events/components/stream/stream_gcp_logging_destination_editor.vue';
+import StreamDeleteModal from 'ee/audit_events/components/stream/stream_delete_modal.vue';
+import { AUDIT_STREAMS_NETWORK_ERRORS, ADD_STREAM_EDITOR_I18N } from 'ee/audit_events/constants';
+import {
+ gcpLoggingDestinationCreateMutationPopulator,
+ gcpLoggingDestinationUpdateMutationPopulator,
+ groupPath,
+ mockGcpLoggingDestination,
+ mockNewGcpLoggingDestination,
+} from '../../mock_data';
+
+jest.mock('~/alert');
+Vue.use(VueApollo);
+
+describe('StreamDestinationEditor', () => {
+ let wrapper;
+
+ const createComponent = ({
+ mountFn = mountExtended,
+ props = {},
+ apolloHandlers = [
+ [
+ googleCloudLoggingConfigurationCreate,
+ jest.fn().mockResolvedValue(gcpLoggingDestinationCreateMutationPopulator()),
+ ],
+ ],
+ } = {}) => {
+ const mockApollo = createMockApollo(apolloHandlers);
+ wrapper = mountFn(StreamGcpLoggingDestinationEditor, {
+ attachTo: document.body,
+ provide: {
+ groupPath,
+ },
+ propsData: {
+ ...props,
+ },
+ apolloProvider: mockApollo,
+ });
+ };
+
+ const findWarningMessage = () => wrapper.findByTestId('data-warning');
+ const findAlertErrors = () => wrapper.findAllByTestId('alert-errors');
+ const findDestinationForm = () => wrapper.findComponent(GlForm);
+ const findAddStreamBtn = () => wrapper.findByTestId('stream-destination-add-button');
+ const findCancelStreamBtn = () => wrapper.findByTestId('stream-destination-cancel-button');
+ const findDeleteBtn = () => wrapper.findByTestId('stream-destination-delete-button');
+ const findDeleteModal = () => wrapper.findComponent(StreamDeleteModal);
+
+ const findProjectIdFormGroup = () =>
+ wrapper.findByTestId('gcp-logging-destination-project-id-form-group');
+ const findProjectId = () => wrapper.findByTestId('gcp-logging-destination-project-id');
+ const findClientEmailFormGroup = () =>
+ wrapper.findByTestId('gcp-logging-destination-client-email-form-group');
+ const findClientEmailUrl = () => wrapper.findByTestId('gcp-logging-destination-client-email');
+ const findLogIdFormGroup = () =>
+ wrapper.findByTestId('gcp-logging-destination-log-id-form-group');
+ const findLogId = () => wrapper.findByTestId('gcp-logging-destination-log-id');
+ const findPasswordFormGroup = () =>
+ wrapper.findByTestId('gcp-logging-destination-password-form-group');
+ const findPassword = () => wrapper.findByTestId('gcp-logging-destination-password');
+
+ afterEach(() => {
+ createAlert.mockClear();
+ });
+
+ describe('Group GCP Logging StreamDestinationEditor', () => {
+ describe('when initialized', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render the destinations warning', () => {
+ expect(findWarningMessage().props('title')).toBe(ADD_STREAM_EDITOR_I18N.WARNING_TITLE);
+ });
+
+ it('should render the destination ProjectId input', () => {
+ expect(findProjectIdFormGroup().exists()).toBe(true);
+ expect(findProjectId().exists()).toBe(true);
+ expect(findProjectId().attributes('placeholder')).toBe(
+ ADD_STREAM_EDITOR_I18N.GCP_LOGGING_DESTINATION_PROJECT_ID_PLACEHOLDER,
+ );
+ });
+
+ it('should render the destination ClientEmail input', () => {
+ expect(findClientEmailFormGroup().exists()).toBe(true);
+ expect(findClientEmailUrl().exists()).toBe(true);
+ expect(findClientEmailUrl().attributes('placeholder')).toBe(
+ ADD_STREAM_EDITOR_I18N.GCP_LOGGING_DESTINATION_CLIENT_EMAIL_PLACEHOLDER,
+ );
+ });
+
+ it('should render the destination IdForm input', () => {
+ expect(findLogIdFormGroup().exists()).toBe(true);
+ expect(findLogId().exists()).toBe(true);
+ expect(findLogId().attributes('placeholder')).toBe(
+ ADD_STREAM_EDITOR_I18N.GCP_LOGGING_DESTINATION_LOG_ID_PLACEHOLDER,
+ );
+ });
+
+ it('should render the destination Password input', () => {
+ expect(findPasswordFormGroup().exists()).toBe(true);
+ expect(findPassword().exists()).toBe(true);
+ });
+
+ it('does not render the delete button', () => {
+ expect(findDeleteBtn().exists()).toBe(false);
+ });
+
+ it('renders the save button text', () => {
+ expect(findAddStreamBtn().text()).toBe(ADD_STREAM_EDITOR_I18N.ADD_BUTTON_TEXT);
+ });
+ });
+
+ describe('add destination event', () => {
+ it('should emit add event after destination added', async () => {
+ createComponent();
+
+ await findProjectId().vm.$emit('input', mockGcpLoggingDestination.googleProjectIdName);
+ await findClientEmailUrl().vm.$emit('input', mockGcpLoggingDestination.clientEmail);
+ await findLogId().vm.$emit('input', mockGcpLoggingDestination.logIdName);
+ await findPassword().vm.$emit('input', mockGcpLoggingDestination.privateKey);
+ await findDestinationForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(findAlertErrors()).toHaveLength(0);
+ expect(wrapper.emitted('error')).toBeUndefined();
+ expect(wrapper.emitted('added')).toBeDefined();
+ });
+
+ it('should not emit add destination event and reports error when server returns error', async () => {
+ const errorMsg = 'Destination hosts limit exceeded';
+ createComponent({
+ apolloHandlers: [
+ [
+ googleCloudLoggingConfigurationCreate,
+ jest.fn().mockResolvedValue(gcpLoggingDestinationCreateMutationPopulator([errorMsg])),
+ ],
+ ],
+ });
+
+ findProjectId().vm.$emit('input', mockGcpLoggingDestination.googleProjectIdName);
+ findClientEmailUrl().vm.$emit('input', mockGcpLoggingDestination.clientEmail);
+ findLogId().vm.$emit('input', mockGcpLoggingDestination.logIdName);
+ findPassword().vm.$emit('input', mockGcpLoggingDestination.privateKey);
+ findDestinationForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(findAlertErrors()).toHaveLength(1);
+ expect(findAlertErrors().at(0).text()).toBe(errorMsg);
+ expect(wrapper.emitted('error')).toBeDefined();
+ expect(wrapper.emitted('added')).toBeUndefined();
+ });
+
+ it('should not emit add destination event and reports error when network error occurs', async () => {
+ const sentryError = new Error('Network error');
+ const sentryCaptureExceptionSpy = jest.spyOn(Sentry, 'captureException');
+ createComponent({
+ apolloHandlers: [
+ [googleCloudLoggingConfigurationCreate, jest.fn().mockRejectedValue(sentryError)],
+ ],
+ });
+
+ findProjectId().vm.$emit('input', mockGcpLoggingDestination.googleProjectIdName);
+ findClientEmailUrl().vm.$emit('input', mockGcpLoggingDestination.clientEmail);
+ findLogId().vm.$emit('input', mockGcpLoggingDestination.logIdName);
+ findPassword().vm.$emit('input', mockGcpLoggingDestination.privateKey);
+ findDestinationForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(findAlertErrors()).toHaveLength(1);
+ expect(findAlertErrors().at(0).text()).toBe(AUDIT_STREAMS_NETWORK_ERRORS.CREATING_ERROR);
+ expect(sentryCaptureExceptionSpy).toHaveBeenCalledWith(sentryError);
+ expect(wrapper.emitted('error')).toBeDefined();
+ expect(wrapper.emitted('added')).toBeUndefined();
+ });
+ });
+
+ describe('cancel event', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit cancel event correctly', () => {
+ findCancelStreamBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('cancel')).toBeDefined();
+ });
+ });
+
+ describe('when editing an existing destination', () => {
+ describe('renders', () => {
+ beforeEach(() => {
+ createComponent({ props: { item: mockGcpLoggingDestination } });
+ });
+
+ it('the destination fields', () => {
+ expect(findProjectId().exists()).toBe(true);
+ expect(findProjectId().element.value).toBe(mockGcpLoggingDestination.googleProjectIdName);
+ expect(findClientEmailUrl().exists()).toBe(true);
+ expect(findClientEmailUrl().element.value).toBe(mockGcpLoggingDestination.clientEmail);
+ expect(findLogId().exists()).toBe(true);
+ expect(findLogId().element.value).toBe(mockGcpLoggingDestination.logIdName);
+ expect(findPassword().exists()).toBe(true);
+ expect(findPassword().element.value).toBe(mockGcpLoggingDestination.privateKey);
+ });
+
+ it('the delete button', () => {
+ expect(findDeleteBtn().exists()).toBe(true);
+ });
+ });
+
+ it('should emit updated event after destination updated', async () => {
+ createComponent({
+ props: { item: mockGcpLoggingDestination },
+ apolloHandlers: [
+ [
+ googleCloudLoggingConfigurationUpdate,
+ jest.fn().mockResolvedValue(gcpLoggingDestinationUpdateMutationPopulator()),
+ ],
+ ],
+ });
+
+ findProjectId().vm.$emit('input', mockNewGcpLoggingDestination.googleProjectIdName);
+ findClientEmailUrl().vm.$emit('input', mockNewGcpLoggingDestination.clientEmail);
+ findLogId().vm.$emit('input', mockNewGcpLoggingDestination.logIdName);
+ findPassword().vm.$emit('input', mockNewGcpLoggingDestination.privateKey);
+ findDestinationForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(findAlertErrors()).toHaveLength(0);
+ expect(wrapper.emitted('error')).toBeUndefined();
+ expect(wrapper.emitted('updated')).toBeDefined();
+ });
+
+ it('should not emit add destination event and reports error when server returns error', async () => {
+ const errorMsg = 'Destination hosts limit exceeded';
+ createComponent({
+ props: { item: mockGcpLoggingDestination },
+ apolloHandlers: [
+ [
+ googleCloudLoggingConfigurationUpdate,
+ jest.fn().mockResolvedValue(gcpLoggingDestinationUpdateMutationPopulator([errorMsg])),
+ ],
+ ],
+ });
+
+ findProjectId().vm.$emit('input', mockGcpLoggingDestination.googleProjectIdName);
+ findClientEmailUrl().vm.$emit('input', mockGcpLoggingDestination.clientEmail);
+ findLogId().vm.$emit('input', mockGcpLoggingDestination.logIdName);
+ findPassword().vm.$emit('input', mockGcpLoggingDestination.privateKey);
+ findDestinationForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(findAlertErrors()).toHaveLength(1);
+ expect(findAlertErrors().at(0).text()).toBe(AUDIT_STREAMS_NETWORK_ERRORS.UPDATING_ERROR);
+ expect(wrapper.emitted('error')).toBeDefined();
+ expect(wrapper.emitted('updated')).toBeUndefined();
+ });
+
+ it('should not emit add destination event and reports error when network error occurs', async () => {
+ const sentryError = new Error('Network error');
+ const sentryCaptureExceptionSpy = jest.spyOn(Sentry, 'captureException');
+ createComponent({
+ props: { item: mockGcpLoggingDestination },
+ apolloHandlers: [
+ [googleCloudLoggingConfigurationUpdate, jest.fn().mockRejectedValue(sentryError)],
+ ],
+ });
+
+ findProjectId().vm.$emit('input', mockGcpLoggingDestination.googleProjectIdName);
+ findClientEmailUrl().vm.$emit('input', mockGcpLoggingDestination.clientEmail);
+ findLogId().vm.$emit('input', mockGcpLoggingDestination.logIdName);
+ findPassword().vm.$emit('input', mockGcpLoggingDestination.privateKey);
+ findDestinationForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(findAlertErrors()).toHaveLength(1);
+ expect(findAlertErrors().at(0).text()).toBe(AUDIT_STREAMS_NETWORK_ERRORS.UPDATING_ERROR);
+ expect(sentryCaptureExceptionSpy).toHaveBeenCalledWith(sentryError);
+ expect(wrapper.emitted('error')).toBeDefined();
+ expect(wrapper.emitted('updated')).toBeUndefined();
+ });
+ });
+
+ describe('deleting', () => {
+ beforeEach(() => {
+ createComponent({ props: { item: mockGcpLoggingDestination } });
+ });
+
+ it('should emit deleted on success operation', async () => {
+ const deleteButton = findDeleteBtn();
+ await deleteButton.trigger('click');
+ await findDeleteModal().vm.$emit('deleting');
+
+ expect(deleteButton.props('loading')).toBe(true);
+
+ await findDeleteModal().vm.$emit('delete');
+
+ expect(deleteButton.props('loading')).toBe(false);
+ expect(wrapper.emitted('deleted')).toEqual([[mockGcpLoggingDestination.id]]);
+ });
+
+ it('shows the alert for the error', () => {
+ const errorMsg = 'An error occurred';
+ findDeleteModal().vm.$emit('error', errorMsg);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: AUDIT_STREAMS_NETWORK_ERRORS.DELETING_ERROR,
+ captureError: true,
+ error: errorMsg,
+ });
+ });
+ });
+ });
+});
diff --git a/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js b/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js
index 99b505bbbd7e511719ad6b1247c2f33c75c7129e..027f403dca01fd694d6c2bdf320712940bcca1b3 100644
--- a/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js
+++ b/ee/spec/frontend/audit_events/components/stream/stream_item_spec.js
@@ -3,11 +3,14 @@ import waitForPromises from 'helpers/wait_for_promises';
import { STREAM_ITEMS_I18N } from 'ee/audit_events/constants';
import StreamItem from 'ee/audit_events/components/stream/stream_item.vue';
import StreamDestinationEditor from 'ee/audit_events/components/stream/stream_destination_editor.vue';
+import StreamGcpLoggingDestinationEditor from 'ee/audit_events/components/stream/stream_gcp_logging_destination_editor.vue';
import {
groupPath,
mockExternalDestinations,
instanceGroupPath,
mockInstanceExternalDestinations,
+ mockHttpType,
+ mockGcpLoggingType,
} from '../../mock_data';
describe('StreamItem', () => {
@@ -18,12 +21,14 @@ describe('StreamItem', () => {
const instanceDestination = mockInstanceExternalDestinations[0];
let groupPathProvide = groupPath;
- let itemProvide = destinationWithoutFilters;
+ let itemProps = destinationWithoutFilters;
+ let typeProps = mockHttpType;
const createComponent = (props = {}) => {
wrapper = mountExtended(StreamItem, {
propsData: {
- item: itemProvide,
+ item: itemProps,
+ type: typeProps,
...props,
},
provide: {
@@ -37,9 +42,10 @@ describe('StreamItem', () => {
const findToggleButton = () => wrapper.findByTestId('toggle-btn');
const findEditor = () => wrapper.findComponent(StreamDestinationEditor);
+ const findGcpLoggingEditor = () => wrapper.findComponent(StreamGcpLoggingDestinationEditor);
const findFilterBadge = () => wrapper.findByTestId('filter-badge');
- describe('Group StreamItem', () => {
+ describe('Group http StreamItem', () => {
describe('render', () => {
beforeEach(() => {
createComponent();
@@ -124,10 +130,75 @@ describe('StreamItem', () => {
});
});
+ describe('Group gcp logging StreamItem', () => {
+ beforeEach(() => {
+ typeProps = mockGcpLoggingType;
+ });
+
+ describe('render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should not render the editor', () => {
+ expect(findGcpLoggingEditor().exists()).toBe(false);
+ });
+ });
+
+ describe('deleting', () => {
+ const id = 1;
+
+ it('bubbles up the "deleted" event', async () => {
+ createComponent();
+ await findToggleButton().vm.$emit('click');
+
+ findGcpLoggingEditor().vm.$emit('deleted', id);
+
+ expect(wrapper.emitted('deleted')).toEqual([[id]]);
+ });
+ });
+
+ describe('editing', () => {
+ beforeEach(async () => {
+ createComponent();
+ await findToggleButton().vm.$emit('click');
+ });
+
+ it('should pass the item to the editor', () => {
+ expect(findGcpLoggingEditor().exists()).toBe(true);
+ expect(findGcpLoggingEditor().props('item')).toStrictEqual(mockExternalDestinations[0]);
+ });
+
+ it('should emit the updated event when the editor fires its update event', async () => {
+ findGcpLoggingEditor().vm.$emit('updated');
+ await waitForPromises();
+
+ expect(wrapper.emitted('updated')).toBeDefined();
+
+ expect(findGcpLoggingEditor().exists()).toBe(false);
+ });
+
+ it('should emit the error event when the editor fires its error event', () => {
+ findGcpLoggingEditor().vm.$emit('error');
+
+ expect(wrapper.emitted('error')).toBeDefined();
+ expect(findGcpLoggingEditor().exists()).toBe(true);
+ });
+
+ it('should close the editor when the editor fires its cancel event', async () => {
+ findGcpLoggingEditor().vm.$emit('cancel');
+ await waitForPromises();
+
+ expect(findGcpLoggingEditor().exists()).toBe(false);
+ });
+ });
+ });
+
describe('Instance StreamItem', () => {
beforeEach(() => {
groupPathProvide = instanceGroupPath;
- itemProvide = instanceDestination;
+ itemProps = instanceDestination;
+ typeProps = mockHttpType;
});
describe('render', () => {
diff --git a/ee/spec/frontend/audit_events/mock_data.js b/ee/spec/frontend/audit_events/mock_data.js
index 65ec7b2e370483bf849949a47431a6a206a583be..8471c3b5f4ee7c8c56b807320479bc66ed595281 100644
--- a/ee/spec/frontend/audit_events/mock_data.js
+++ b/ee/spec/frontend/audit_events/mock_data.js
@@ -41,6 +41,9 @@ export default () => [
populateEvent('User 4', false, false),
];
+export const mockHttpType = 'http';
+export const mockGcpLoggingType = 'gcpLogging';
+
export const mockExternalDestinationUrl = 'https://api.gitlab.com';
export const mockExternalDestinationHeader = () => ({
id: uniqueId('gid://gitlab/AuditEvents::Streaming::Header/'),
@@ -112,6 +115,24 @@ export const mockInstanceExternalDestinations = [
},
];
+export const mockGcpLoggingDestination = {
+ __typename: 'GoogleCloudLoggingConfigurationType',
+ id: 'gid://gitlab/AuditEvents::GoogleCloudLoggingConfiguration/1',
+ clientEmail: 'my-email@my-google-project.iam.gservice.account.com',
+ googleProjectIdName: 'my-google-project',
+ logIdName: 'audit-events',
+ privateKey: 'PRIVATE_KEY',
+};
+
+export const mockNewGcpLoggingDestination = {
+ __typename: 'GoogleCloudLoggingConfigurationType',
+ id: 'gid://gitlab/AuditEvents::GoogleCloudLoggingConfiguration/1',
+ clientEmail: 'new-email@my-google-project.iam.gservice.account.com',
+ googleProjectIdName: 'new-google-project',
+ logIdName: 'audit-events',
+ privateKey: 'PRIVATE_KEY',
+};
+
export const groupPath = 'test-group';
export const instanceGroupPath = 'instance';
@@ -151,7 +172,7 @@ export const destinationCreateMutationPopulator = (errors = []) => {
const errorData = {
errors,
- externalAuditEventDestination: null,
+ googleCloudLoggingConfiguration: null,
};
return {
@@ -161,6 +182,42 @@ export const destinationCreateMutationPopulator = (errors = []) => {
};
};
+export const gcpLoggingDestinationCreateMutationPopulator = (errors = []) => {
+ const correctData = {
+ errors,
+ googleCloudLoggingConfiguration: mockGcpLoggingDestination,
+ };
+
+ const errorData = {
+ errors,
+ googleCloudLoggingConfiguration: null,
+ };
+
+ return {
+ data: {
+ googleCloudLoggingConfigurationCreate: errors.length > 0 ? errorData : correctData,
+ },
+ };
+};
+
+export const gcpLoggingDestinationUpdateMutationPopulator = (errors = []) => {
+ const correctData = {
+ errors,
+ googleCloudLoggingConfiguration: mockGcpLoggingDestination,
+ };
+
+ const errorData = {
+ errors,
+ externalAuditEventDestination: null,
+ };
+
+ return {
+ data: {
+ googleCloudLoggingConfigurationUpdate: errors.length > 0 ? errorData : correctData,
+ },
+ };
+};
+
export const destinationDeleteMutationPopulator = (errors = []) => ({
data: {
externalAuditEventDestinationDestroy: {
@@ -306,3 +363,11 @@ export const destinationInstanceDeleteMutationPopulator = (errors = []) => ({
},
},
});
+
+export const destinationGcpLoggingDeleteMutationPopulator = (errors = []) => ({
+ data: {
+ googleCloudLoggingConfigurationDestroy: {
+ errors,
+ },
+ },
+});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 78b7c5238e92c1ba2822ee704c5c1c5b6951f0d6..b863867169b73d45a4d5dd8cf972e003506eee1e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6564,6 +6564,9 @@ msgstr ""
msgid "AuditStreams|Cancel editing"
msgstr ""
+msgid "AuditStreams|Client Email"
+msgstr ""
+
msgid "AuditStreams|Custom HTTP headers (optional)"
msgstr ""
@@ -6588,15 +6591,30 @@ msgstr ""
msgid "AuditStreams|Filter by audit event type"
msgstr ""
+msgid "AuditStreams|Google Cloud Logging"
+msgstr ""
+
+msgid "AuditStreams|HTTP endpoint"
+msgstr ""
+
msgid "AuditStreams|Header"
msgstr ""
+msgid "AuditStreams|Log ID"
+msgstr ""
+
msgid "AuditStreams|Maximum of %{number} HTTP headers has been reached."
msgstr ""
msgid "AuditStreams|No header created yet."
msgstr ""
+msgid "AuditStreams|Private key"
+msgstr ""
+
+msgid "AuditStreams|Project ID"
+msgstr ""
+
msgid "AuditStreams|Remove custom header"
msgstr ""
@@ -6633,6 +6651,9 @@ msgstr ""
msgid "AuditStreams|Verification token"
msgstr ""
+msgid "AuditStreams|audit-events"
+msgstr ""
+
msgid "AuditStreams|ex: 1000"
msgstr ""
@@ -6642,6 +6663,12 @@ msgstr ""
msgid "AuditStreams|filtered"
msgstr ""
+msgid "AuditStreams|my-email@my-google-project.iam.gservice.account.com"
+msgstr ""
+
+msgid "AuditStreams|my-google-project"
+msgstr ""
+
msgid "Aug"
msgstr ""