summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue40
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue26
-rw-r--r--app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json103
-rw-r--r--app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json78
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql18
-rw-r--r--app/assets/javascripts/alerts_settings/utils/mapping_transformations.js63
-rw-r--r--doc/api/oauth2.md30
-rw-r--r--doc/development/integrations/secure.md2
-rw-r--r--doc/operations/incident_management/alert_notifications.md8
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb2
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap (renamed from spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap)0
-rw-r--r--spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js (renamed from spec/frontend/alerts_settings/alert_mapping_builder_spec.js)38
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js (renamed from spec/frontend/alerts_settings/alerts_integrations_list_spec.js)0
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js (renamed from spec/frontend/alerts_settings/alerts_settings_form_spec.js)199
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js (renamed from spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js)0
-rw-r--r--spec/frontend/alerts_settings/components/mocks/apollo_mock.js (renamed from spec/frontend/alerts_settings/mocks/apollo_mock.js)0
-rw-r--r--spec/frontend/alerts_settings/components/mocks/integrations.json (renamed from spec/frontend/alerts_settings/mocks/integrations.json)0
-rw-r--r--spec/frontend/alerts_settings/components/util.js (renamed from spec/frontend/alerts_settings/util.js)0
-rw-r--r--spec/frontend/alerts_settings/utils/mapping_transformations_spec.js80
22 files changed, 440 insertions, 253 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
index c52e9f5c264..8a481db5cb6 100644
--- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
+++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
@@ -13,6 +13,12 @@ import { s__, __ } from '~/locale';
// data format is defined and will be the same as mocked (maybe with some minor changes)
// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
import gitlabFieldsMock from './mocks/gitlabFields.json';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import {
+ getMappingData,
+ getPayloadFields,
+ transformForSave,
+} from '../utils/mapping_transformations';
export const i18n = {
columns: {
@@ -46,12 +52,12 @@ export default {
},
},
props: {
- payloadFields: {
+ parsedPayload: {
type: Array,
required: false,
default: () => [],
},
- mapping: {
+ savedMapping: {
type: Array,
required: false,
default: () => [],
@@ -63,27 +69,11 @@ export default {
};
},
computed: {
+ payloadFields() {
+ return getPayloadFields(this.parsedPayload);
+ },
mappingData() {
- return this.gitlabFields.map((gitlabField) => {
- const mappingFields = this.payloadFields.filter(({ type }) =>
- type.some((t) => gitlabField.compatibleTypes.includes(t)),
- );
-
- const foundMapping = this.mapping.find(
- ({ alertFieldName }) => alertFieldName === gitlabField.name,
- );
-
- const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
-
- return {
- mapping: payloadAlertPaths,
- fallback: fallbackAlertPaths,
- searchTerm: '',
- fallbackSearchTerm: '',
- mappingFields,
- ...gitlabField,
- };
- });
+ return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping);
},
},
methods: {
@@ -91,6 +81,7 @@ export default {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } };
Vue.set(this.gitlabFields, fieldIndex, updatedField);
+ this.$emit('onMappingUpdate', transformForSave(this.mappingData));
},
setSearchTerm(search = '', searchFieldKey, gitlabKey) {
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
@@ -99,7 +90,6 @@ export default {
},
filterFields(searchTerm = '', fields) {
const search = searchTerm.toLowerCase();
-
return fields.filter((field) => field.label.toLowerCase().includes(search));
},
isSelected(fieldValue, mapping) {
@@ -112,7 +102,9 @@ export default {
);
},
getFieldValue({ label, type }) {
- return `${label} (${type.join(__(' or '))})`;
+ const types = type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(__(' or '));
+
+ return `${label} (${types})`;
},
noResults(searchTerm, fields) {
return !this.filterFields(searchTerm, fields).length;
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 1ae7f826ce6..3280e0424aa 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -152,6 +152,7 @@ export default {
},
resetSamplePayloadConfirmed: false,
customMapping: null,
+ mapping: [],
parsingPayload: false,
currentIntegration: null,
};
@@ -199,10 +200,10 @@ export default {
this.selectedIntegration === typeSet.http
);
},
- mappingBuilderFields() {
+ parsedSamplePayload() {
return this.customMapping?.samplePayload?.payloadAlerFields?.nodes;
},
- mappingBuilderMapping() {
+ savedMapping() {
return this.customMapping?.storedMapping?.nodes;
},
hasSamplePayload() {
@@ -255,9 +256,20 @@ export default {
},
submit() {
const { name, apiUrl } = this.integrationForm;
+ const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
+ ? {
+ payloadAttributeMappings: this.mapping,
+ payloadExample: this.integrationTestPayload.json,
+ }
+ : {};
+
const variables =
this.selectedIntegration === typeSet.http
- ? { name, active: this.active }
+ ? {
+ name,
+ active: this.active,
+ ...customMappingVariables,
+ }
: { apiUrl, active: this.active };
const integrationPayload = { type: this.selectedIntegration, variables };
@@ -336,6 +348,9 @@ export default {
this.integrationTestPayload.json = res?.samplePayload.body;
});
},
+ updateMapping(mapping) {
+ this.mapping = mapping;
+ },
},
};
</script>
@@ -541,8 +556,9 @@ export default {
>
<span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
<mapping-builder
- :payload-fields="mappingBuilderFields"
- :mapping="mappingBuilderMapping"
+ :parsed-payload="parsedSamplePayload"
+ :saved-mapping="savedMapping"
+ @onMappingUpdate="updateMapping"
/>
</gl-form-group>
</div>
diff --git a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json
index ac559a30eda..e4d0e92a6f8 100644
--- a/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json
+++ b/app/assets/javascripts/alerts_settings/components/mocks/gitlabFields.json
@@ -1,112 +1,123 @@
[
{
- "name": "title",
+ "name": "TITLE",
"label": "Title",
"type": [
- "String"
+ "STRING"
],
"compatibleTypes": [
- "String",
- "Number",
- "DateTime"
+ "STRING",
+ "NUMBER",
+ "DATETIME"
],
"numberOfFallbacks": 1
},
{
- "name": "description",
+ "name": "DESCRIPTION",
"label": "Description",
"type": [
- "String"
+ "STRING"
],
"compatibleTypes": [
- "String",
- "Number",
- "DateTime"
+ "STRING",
+ "NUMBER",
+ "DATETIME"
]
},
{
- "name": "startTime",
+ "name": "START_TIME",
"label": "Start time",
"type": [
- "DateTime"
+ "DATETIME"
],
"compatibleTypes": [
- "Number",
- "DateTime"
+ "NUMBER",
+ "DATETIME"
]
},
{
- "name": "service",
+ "name": "END_TIME",
+ "label": "End time",
+ "type": [
+ "DATETIME"
+ ],
+ "compatibleTypes": [
+ "NUMBER",
+ "DATETIME"
+ ]
+ },
+ {
+ "name": "SERVICE",
"label": "Service",
"type": [
- "String"
+ "STRING"
],
"compatibleTypes": [
- "String",
- "Number",
- "DateTime"
+ "STRING",
+ "NUMBER",
+ "DATETIME"
]
},
{
- "name": "monitoringTool",
+ "name": "MONITORING_TOOL",
"label": "Monitoring tool",
"type": [
- "String"
+ "STRING"
],
"compatibleTypes": [
- "String",
- "Number",
- "DateTime"
+ "STRING",
+ "NUMBER",
+ "DATETIME"
]
},
{
- "name": "hosts",
+ "name": "HOSTS",
"label": "Hosts",
"type": [
- "String",
- "Array"
+ "STRING",
+ "ARRAY"
],
"compatibleTypes": [
- "String",
- "Array",
- "Number",
- "DateTime"
+ "STRING",
+ "ARRAY",
+ "NUMBER",
+ "DATETIME"
]
},
{
- "name": "severity",
+ "name": "SEVERITY",
"label": "Severity",
"type": [
- "String"
+ "STRING"
],
"compatibleTypes": [
- "String",
- "Number",
- "DateTime"
+ "STRING",
+ "NUMBER",
+ "DATETIME"
]
},
{
- "name": "fingerprint",
+ "name": "FINGERPRINT",
"label": "Fingerprint",
"type": [
- "String"
+ "STRING"
],
"compatibleTypes": [
- "String",
- "Number",
- "DateTime"
+ "STRING",
+ "NUMBER",
+ "DATETIME"
]
},
{
- "name": "environment",
+ "name": "GITLAB_ENVIRONMENT_NAME",
"label": "Environment",
"type": [
- "String"
+ "STRING"
],
"compatibleTypes": [
- "String",
- "Number",
- "DateTime"
+ "STRING",
+ "NUMBER",
+ "DATETIME"
]
}
]
diff --git a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
index 5326678155d..c1de0d6f0e0 100644
--- a/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
+++ b/app/assets/javascripts/alerts_settings/components/mocks/parsedMapping.json
@@ -4,95 +4,69 @@
"payloadAlerFields": {
"nodes": [
{
- "name": "dashboardId",
+ "path": ["dashboardId"],
"label": "Dashboard Id",
- "type": [
- "Number"
- ]
+ "type": "STRING"
},
{
- "name": "evalMatches",
+ "path": ["evalMatches"],
"label": "Eval Matches",
- "type": [
- "Array"
- ]
+ "type": "ARRAY"
},
{
- "name": "createdAt",
+ "path": ["createdAt"],
"label": "Created At",
- "type": [
- "DateTime"
- ]
+ "type": "DATETIME"
},
{
- "name": "imageUrl",
+ "path": ["imageUrl"],
"label": "Image Url",
- "type": [
- "String"
- ]
+ "type": "STRING"
},
{
- "name": "message",
+ "path": ["message"],
"label": "Message",
- "type": [
- "String"
- ]
+ "type": "STRING"
},
{
- "name": "orgId",
+ "path": ["orgId"],
"label": "Org Id",
- "type": [
- "Number"
- ]
+ "type": "STRING"
},
{
- "name": "panelId",
+ "path": ["panelId"],
"label": "Panel Id",
- "type": [
- "String"
- ]
+ "type": "STRING"
},
{
- "name": "ruleId",
+ "path": ["ruleId"],
"label": "Rule Id",
- "type": [
- "Number"
- ]
+ "type": "STRING"
},
{
- "name": "ruleName",
+ "path": ["ruleName"],
"label": "Rule Name",
- "type": [
- "String"
- ]
+ "type": "STRING"
},
{
- "name": "ruleUrl",
+ "path": ["ruleUrl"],
"label": "Rule Url",
- "type": [
- "String"
- ]
+ "type": "STRING"
},
{
- "name": "state",
+ "path": ["state"],
"label": "State",
- "type": [
- "String"
- ]
+ "type": "STRING"
},
{
- "name": "title",
+ "path": ["title"],
"label": "Title",
- "type": [
- "String"
- ]
+ "type": "STRING"
},
{
- "name": "tags",
+ "path": ["tags", "tag"],
"label": "Tags",
- "type": [
- "Object"
- ]
+ "type": "STRING"
}
]
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
index d1dacbad40a..f3fc10b4bd4 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql
@@ -1,7 +1,21 @@
#import "../fragments/integration_item.fragment.graphql"
-mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
- httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
+mutation createHttpIntegration(
+ $projectPath: ID!
+ $name: String!
+ $active: Boolean!
+ $payloadExample: JsonString
+ $payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
+) {
+ httpIntegrationCreate(
+ input: {
+ projectPath: $projectPath
+ name: $name
+ active: $active
+ payloadExample: $payloadExample
+ payloadAttributeMappings: $payloadAttributeMappings
+ }
+ ) {
errors
integration {
...IntegrationItem
diff --git a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
new file mode 100644
index 00000000000..a7e43c93fbf
--- /dev/null
+++ b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
@@ -0,0 +1,63 @@
+/**
+ * Given data for GitLab alert fields, parsed payload fields data and previously stored mapping (if any)
+ * creates an object in a form convenient to build UI && interact with it
+ * @param {Object} gitlabFields - structure describing GitLab alert fields
+ * @param {Object} payloadFields - parsed from sample JSON sample alert fields
+ * @param {Object} savedMapping - GitLab fields to parsed fields mapping
+ *
+ * @return {Object} mapping data for UI mapping builder
+ */
+export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
+ return gitlabFields.map((gitlabField) => {
+ // find fields from payload that match gitlab alert field by type
+ const mappingFields = payloadFields.filter(({ type }) =>
+ gitlabField.compatibleTypes.includes(type),
+ );
+
+ // find the mapping that was previously stored
+ const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name);
+
+ const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
+
+ return {
+ mapping: payloadAlertPaths,
+ fallback: fallbackAlertPaths,
+ searchTerm: '',
+ fallbackSearchTerm: '',
+ mappingFields,
+ ...gitlabField,
+ };
+ });
+};
+
+/**
+ * Based on mapping data configured by the user creates an object in a format suitable for save on BE
+ * @param {Object} mappingData - structure describing mapping between GitLab fields and parsed payload fields
+ *
+ * @return {Object} mapping data to send to BE
+ */
+export const transformForSave = (mappingData) => {
+ return mappingData.reduce((acc, field) => {
+ const mapped = field.mappingFields.find(({ name }) => name === field.mapping);
+ if (mapped) {
+ const { path, type, label } = mapped;
+ acc.push({
+ fieldName: field.name,
+ path,
+ type,
+ label,
+ });
+ }
+ return acc;
+ }, []);
+};
+
+/**
+ * Adds `name` prop to each provided by BE parsed payload field
+ * @param {Object} payload - parsed sample payload
+ *
+ * @return {Object} same as input with an extra `name` property which basically serves as a key to make a match
+ */
+export const getPayloadFields = (payload) => {
+ return payload.map((field) => ({ ...field, name: field.path.join('_') }));
+};
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index a80a97890ba..9f20fd540a4 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -89,7 +89,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD
`/oauth/authorize` page with the following query parameters:
```plaintext
- https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH&scope=REQUESTED_SCOPES&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
+ https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
```
This page asks the user to approve the request from the app to access their
@@ -100,7 +100,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD
The redirect includes the authorization `code`, for example:
```plaintext
- https://example.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH
+ https://example.com/oauth/redirect?code=1234567890&state=STATE
```
1. With the authorization `code` returned from the previous request (denoted as
@@ -139,29 +139,31 @@ detailed flow description.
The authorization code flow is essentially the same as
[authorization code flow with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce),
+Before starting the flow, generate the `STATE`. It is a value that can't be predicted
+used by the client to maintain state between the request and callback. It should also
+be used as a CSRF token.
+
1. Request authorization code. To do that, you should redirect the user to the
- `/oauth/authorize` endpoint with the following GET parameters:
+ `/oauth/authorize` page with the following query parameters:
```plaintext
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES
```
- This will ask the user to approve the applications access to their account
- based on the scopes specified in `REQUESTED_SCOPES` and then redirect back to
- the `REDIRECT_URI` you provided. The [scope parameter](https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes#requesting-particular-scopes)
- is a space separated list of scopes you want to have access to (e.g. `scope=read_user+profile`
- would request `read_user` and `profile` scopes). The redirect will
- include the GET `code` parameter, for example:
+ This page asks the user to approve the request from the app to access their
+ account based on the scopes specified in `REQUESTED_SCOPES`. The user is then
+ redirected back to the specified `REDIRECT_URI`. The [scope parameter](https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes#requesting-particular-scopes)
+ is a space separated list of scopes associated with the user.
+ For example,`scope=read_user+profile` requests the `read_user` and `profile` scopes.
+ The redirect includes the authorization `code`, for example:
```plaintext
https://example.com/oauth/redirect?code=1234567890&state=STATE
```
- You should then use `code` to request an access token.
-
-1. After you have the authorization code you can request an `access_token` using the
- code. You can do that by using any HTTP client. In the following example,
- we are using Ruby's `rest-client`:
+1. With the authorization `code` returned from the previous request (shown as
+ `RETURNED_CODE` in the following example), you can request an `access_token`, with
+ any HTTP client. The following example uses Ruby's `rest-client`:
```ruby
parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md
index fb9d894d203..1d5aced5869 100644
--- a/doc/development/integrations/secure.md
+++ b/doc/development/integrations/secure.md
@@ -260,6 +260,8 @@ When executing command lines, scanners should use the `debug` level to log the c
For instance, the [bundler-audit](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit) scanner
uses the `debug` level to log the command line `bundle audit check --quiet`,
and what `bundle audit` writes to the standard output.
+If the command line fails, then it should be logged with the `error` log level;
+this makes it possible to debug the problem without having to change the log level to `debug` and rerun the scanning job.
#### common logutil package
diff --git a/doc/operations/incident_management/alert_notifications.md b/doc/operations/incident_management/alert_notifications.md
new file mode 100644
index 00000000000..6f3b329572b
--- /dev/null
+++ b/doc/operations/incident_management/alert_notifications.md
@@ -0,0 +1,8 @@
+---
+redirect_to: 'paging.md'
+---
+
+This document was moved to [another location](paging.md).
+
+<!-- This redirect file can be deleted after <2022-01-21>. -->
+<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb
index 469335db5ab..604c98ca21e 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Multiple file snippet' do
+ describe 'Multiple file snippet', quarantine: { only: { pipeline: :master }, issue: 'https://gitlab.com/gitlab-org/gitaly/-/issues/3143', type: :bug } do
let(:personal_snippet) do
Resource::Snippet.fabricate_via_api! do |snippet|
snippet.title = 'Personal snippet to add file to'
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
index a21c5d58aad..2940b2067a1 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Version control for personal snippets' do
+ describe 'Version control for personal snippets', quarantine: { only: { pipeline: :master }, issue: 'https://gitlab.com/gitlab-org/gitaly/-/issues/3143', type: :bug } do
let(:new_file) { 'new_snippet_file' }
let(:changed_content) { 'changes' }
let(:commit_message) { 'Changes to snippets' }
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb
index 4ce6c3fdcd3..b91424d5b65 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Version control for project snippets' do
+ describe 'Version control for project snippets', quarantine: { only: { pipeline: :master }, issue: 'https://gitlab.com/gitlab-org/gitaly/-/issues/3143', type: :bug } do
let(:new_file) { 'new_snippet_file' }
let(:changed_content) { 'changes' }
let(:commit_message) { 'Changes to snippets' }
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb
index 8002e95cf0d..5c0983dabb6 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Multiple file snippet' do
+ describe 'Multiple file snippet', quarantine: { only: { pipeline: :master }, issue: 'https://gitlab.com/gitlab-org/gitaly/-/issues/3143', type: :bug } do
let(:personal_snippet) do
Resource::Snippet.fabricate_via_api! do |snippet|
snippet.title = 'Personal snippet to delete file from'
diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
index ef68a6a2c32..ef68a6a2c32 100644
--- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
diff --git a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
index 5d48ff02e35..92ea8b2f33c 100644
--- a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js
+++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
@@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json';
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations';
describe('AlertMappingBuilder', () => {
let wrapper;
@@ -10,8 +12,8 @@ describe('AlertMappingBuilder', () => {
function mountComponent() {
wrapper = shallowMount(AlertMappingBuilder, {
propsData: {
- payloadFields: parsedMapping.samplePayload.payloadAlerFields.nodes,
- mapping: parsedMapping.storedMapping.nodes,
+ parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
+ savedMapping: parsedMapping.storedMapping.nodes,
},
});
}
@@ -44,7 +46,8 @@ describe('AlertMappingBuilder', () => {
it('renders disabled form input for each mapped field', () => {
gitlabFields.forEach((field, index) => {
const input = findColumnInRow(index + 1, 0).find(GlFormInput);
- expect(input.attributes('value')).toBe(`${field.label} (${field.type.join(' or ')})`);
+ const types = field.type.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
+ expect(input.attributes('value')).toBe(`${field.label} (${types})`);
expect(input.attributes('disabled')).toBe('');
});
});
@@ -59,16 +62,14 @@ describe('AlertMappingBuilder', () => {
it('renders mapping dropdown for each field', () => {
gitlabFields.forEach(({ compatibleTypes }, index) => {
const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
- const searchBox = dropdown.find(GlSearchBoxByType);
- const dropdownItems = dropdown.findAll(GlDropdownItem);
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
- const numberOfMappingOptions = nodes.filter(({ type }) =>
- type.some((t) => compatibleTypes.includes(t)),
- );
+ const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type));
expect(dropdown.exists()).toBe(true);
expect(searchBox.exists()).toBe(true);
- expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
});
});
@@ -78,16 +79,23 @@ describe('AlertMappingBuilder', () => {
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
if (numberOfFallbacks) {
- const searchBox = dropdown.find(GlSearchBoxByType);
- const dropdownItems = dropdown.findAll(GlDropdownItem);
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
- const numberOfMappingOptions = nodes.filter(({ type }) =>
- type.some((t) => compatibleTypes.includes(t)),
- );
+ const mappingOptions = nodes.filter(({ type }) => compatibleTypes.includes(type));
expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
- expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
}
});
});
+
+ it('emits event with selected mapping', () => {
+ const mappingToSave = { fieldName: 'TITLE', mapping: 'PARSED_TITLE' };
+ jest.spyOn(transformationUtils, 'transformForSave').mockReturnValue(mappingToSave);
+ const dropdown = findColumnInRow(1, 2).find(GlDropdown);
+ const option = dropdown.find(GlDropdownItem);
+ option.vm.$emit('click');
+ expect(wrapper.emitted('onMappingUpdate')[0]).toEqual([mappingToSave]);
+ });
});
diff --git a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
index 5a3874d055b..5a3874d055b 100644
--- a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
diff --git a/spec/frontend/alerts_settings/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 21cdec6f94c..4f8983b1aca 100644
--- a/spec/frontend/alerts_settings/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -9,6 +9,7 @@ import {
} from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
+import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
import { defaultAlertSettingsConfig } from './util';
import { typeSet } from '~/alerts_settings/constants';
@@ -49,6 +50,7 @@ describe('AlertsSettingsFormNew', () => {
const findFormToggle = () => wrapper.find(GlToggle);
const findTestPayloadSection = () => wrapper.find(`[id = "test-integration"]`);
const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
+ const findMappingBuilder = () => wrapper.findComponent(MappingBuilder);
const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
const findMultiSupportText = () =>
wrapper.find(`[data-testid="multi-integrations-not-supported"]`);
@@ -63,6 +65,16 @@ describe('AlertsSettingsFormNew', () => {
}
});
+ const selectOptionAtIndex = async (index) => {
+ const options = findSelect().findAll('option');
+ await options.at(index).setSelected();
+ };
+
+ const enableIntegration = (index, value) => {
+ findFormFields().at(index).setValue(value);
+ findFormToggle().trigger('click');
+ };
+
describe('with default values', () => {
beforeEach(() => {
createComponent();
@@ -80,10 +92,7 @@ describe('AlertsSettingsFormNew', () => {
});
it('shows the rest of the form when the dropdown is used', async () => {
- const options = findSelect().findAll('option');
- await options.at(1).setSelected();
-
- await wrapper.vm.$nextTick();
+ await selectOptionAtIndex(1);
expect(findFormFields().at(0).isVisible()).toBe(true);
});
@@ -96,120 +105,128 @@ describe('AlertsSettingsFormNew', () => {
it('disabled the name input when the selected value is prometheus', async () => {
createComponent();
- const options = findSelect().findAll('option');
- await options.at(2).setSelected();
+ await selectOptionAtIndex(2);
expect(findFormFields().at(0).attributes('disabled')).toBe('disabled');
});
});
describe('submitting integration form', () => {
- it('allows for create-new-integration with the correct form values for HTTP', async () => {
- createComponent();
+ describe('HTTP', () => {
+ it('create', async () => {
+ createComponent();
- const options = findSelect().findAll('option');
- await options.at(1).setSelected();
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
+ enableIntegration(0, integrationName);
- await findFormFields().at(0).setValue('Test integration');
- await findFormToggle().trigger('click');
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- await wrapper.vm.$nextTick();
-
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
-
- findForm().trigger('submit');
+ findForm().trigger('submit');
- await wrapper.vm.$nextTick();
-
- expect(wrapper.emitted('create-new-integration')).toBeTruthy();
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- { type: typeSet.http, variables: { name: 'Test integration', active: true } },
- ]);
- });
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: integrationName, active: true } },
+ ]);
+ });
- it('allows for create-new-integration with the correct form values for PROMETHEUS', async () => {
- createComponent();
+ it('create with custom mapping', async () => {
+ createComponent({ multipleHttpIntegrationsCustomMapping: true });
- const options = findSelect().findAll('option');
- await options.at(2).setSelected();
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
- await findFormFields().at(0).setValue('Test integration');
- await findFormFields().at(1).setValue('https://test.com');
- await findFormToggle().trigger('click');
+ enableIntegration(0, integrationName);
- await wrapper.vm.$nextTick();
+ const sampleMapping = { field: 'test' };
+ findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
+ findForm().trigger('submit');
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ {
+ type: typeSet.http,
+ variables: {
+ name: integrationName,
+ active: true,
+ payloadAttributeMappings: sampleMapping,
+ payloadExample: null,
+ },
+ },
+ ]);
+ });
- findForm().trigger('submit');
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.http,
+ currentIntegration: { id: '1', name: 'Test integration pre' },
+ },
+ props: {
+ loading: false,
+ },
+ });
+ const updatedIntegrationName = 'Test integration post';
+ enableIntegration(0, updatedIntegrationName);
- await wrapper.vm.$nextTick();
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- expect(wrapper.emitted('create-new-integration')).toBeTruthy();
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } },
- ]);
- });
+ findForm().trigger('submit');
- it('allows for update-integration with the correct form values for HTTP', async () => {
- createComponent({
- data: {
- selectedIntegration: typeSet.http,
- currentIntegration: { id: '1', name: 'Test integration pre' },
- },
- props: {
- loading: false,
- },
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: updatedIntegrationName, active: true } },
+ ]);
});
+ });
- await findFormFields().at(0).setValue('Test integration post');
- await findFormToggle().trigger('click');
+ describe('PROMETHEUS', () => {
+ it('create', async () => {
+ createComponent();
- await wrapper.vm.$nextTick();
+ await selectOptionAtIndex(2);
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ const apiUrl = 'https://test.com';
+ enableIntegration(1, apiUrl);
- findForm().trigger('submit');
+ findFormToggle().trigger('click');
- await wrapper.vm.$nextTick();
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- expect(wrapper.emitted('update-integration')).toBeTruthy();
- expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.http, variables: { name: 'Test integration post', active: true } },
- ]);
- });
+ findForm().trigger('submit');
- it('allows for update-integration with the correct form values for PROMETHEUS', async () => {
- createComponent({
- data: {
- selectedIntegration: typeSet.prometheus,
- currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
- },
- props: {
- loading: false,
- },
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
});
- await findFormFields().at(0).setValue('Test integration');
- await findFormFields().at(1).setValue('https://test-post.com');
- await findFormToggle().trigger('click');
-
- await wrapper.vm.$nextTick();
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.prometheus,
+ currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
+ },
+ props: {
+ loading: false,
+ },
+ });
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ const apiUrl = 'https://test-post.com';
+ enableIntegration(1, apiUrl);
- findForm().trigger('submit');
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- await wrapper.vm.$nextTick();
+ findForm().trigger('submit');
- expect(wrapper.emitted('update-integration')).toBeTruthy();
- expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } },
- ]);
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
+ });
});
});
@@ -234,9 +251,10 @@ describe('AlertsSettingsFormNew', () => {
jest.runAllTimers();
await wrapper.vm.$nextTick();
- expect(findJsonTestSubmit().exists()).toBe(true);
- expect(findJsonTestSubmit().text()).toBe('Save and test payload');
- expect(findJsonTestSubmit().props('disabled')).toBe(true);
+ const jsonTestSubmit = findJsonTestSubmit();
+ expect(jsonTestSubmit.exists()).toBe(true);
+ expect(jsonTestSubmit.text()).toBe('Save and test payload');
+ expect(jsonTestSubmit.props('disabled')).toBe(true);
});
it('should allow for the form to be automatically saved if the test payload is successfully submitted', async () => {
@@ -341,9 +359,8 @@ describe('AlertsSettingsFormNew', () => {
it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => {
createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag });
- const options = findSelect().findAll('option');
- options.at(integrationOption).setSelected();
- await wrapper.vm.$nextTick();
+ await selectOptionAtIndex(integrationOption);
+
expect(findMappingBuilderSection().exists()).toBe(visible);
});
});
diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 4d0732ca76c..4d0732ca76c 100644
--- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
diff --git a/spec/frontend/alerts_settings/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
index e0eba1e8421..e0eba1e8421 100644
--- a/spec/frontend/alerts_settings/mocks/apollo_mock.js
+++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
diff --git a/spec/frontend/alerts_settings/mocks/integrations.json b/spec/frontend/alerts_settings/components/mocks/integrations.json
index b1284fc55a2..b1284fc55a2 100644
--- a/spec/frontend/alerts_settings/mocks/integrations.json
+++ b/spec/frontend/alerts_settings/components/mocks/integrations.json
diff --git a/spec/frontend/alerts_settings/util.js b/spec/frontend/alerts_settings/components/util.js
index 5c07f22f1c9..5c07f22f1c9 100644
--- a/spec/frontend/alerts_settings/util.js
+++ b/spec/frontend/alerts_settings/components/util.js
diff --git a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js
new file mode 100644
index 00000000000..f725712fdd2
--- /dev/null
+++ b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js
@@ -0,0 +1,80 @@
+import {
+ getMappingData,
+ getPayloadFields,
+ transformForSave,
+} from '~/alerts_settings/utils/mapping_transformations';
+import gitlabFieldsMock from '~/alerts_settings/components/mocks/gitlabFields.json';
+import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
+
+describe('Mapping Transformation Utilities', () => {
+ const nameField = {
+ label: 'Name',
+ path: ['alert', 'name'],
+ type: 'STRING',
+ };
+ const dashboardField = {
+ label: 'Dashboard Id',
+ path: ['alert', 'dashboardId'],
+ type: 'STRING',
+ };
+
+ describe('getMappingData', () => {
+ it('should return mapping data', () => {
+ const alertFields = gitlabFieldsMock.slice(0, 3);
+ const result = getMappingData(
+ alertFields,
+ getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)),
+ parsedMapping.storedMapping.nodes.slice(0, 3),
+ );
+
+ result.forEach((data, index) => {
+ expect(data).toEqual(
+ expect.objectContaining({
+ ...alertFields[index],
+ searchTerm: '',
+ fallbackSearchTerm: '',
+ }),
+ );
+ });
+ });
+ });
+
+ describe('transformForSave', () => {
+ it('should transform mapped data for save', () => {
+ const fieldName = 'title';
+ const mockMappingData = [
+ {
+ name: fieldName,
+ mapping: 'alert_name',
+ mappingFields: getPayloadFields([dashboardField, nameField]),
+ },
+ ];
+ const result = transformForSave(mockMappingData);
+ const { path, type, label } = nameField;
+ expect(result).toEqual([{ fieldName, path, type, label }]);
+ });
+
+ it('should return empty array if no mapping provided', () => {
+ const fieldName = 'title';
+ const mockMappingData = [
+ {
+ name: fieldName,
+ mapping: null,
+ mappingFields: getPayloadFields([nameField, dashboardField]),
+ },
+ ];
+ const result = transformForSave(mockMappingData);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('getPayloadFields', () => {
+ it('should add name field to each payload field', () => {
+ const result = getPayloadFields([nameField, dashboardField]);
+ expect(result).toEqual([
+ { ...nameField, name: 'alert_name' },
+ { ...dashboardField, name: 'alert_dashboardId' },
+ ]);
+ });
+ });
+});