Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
21796f414c
commit
6ef43e2aa1
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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('_') }));
|
||||
};
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 -->
|
|
@ -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'
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -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,64 +105,58 @@ 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 () => {
|
||||
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');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findSubmitButton().exists()).toBe(true);
|
||||
expect(findSubmitButton().text()).toBe('Save integration');
|
||||
const submitBtn = findSubmitButton();
|
||||
expect(submitBtn.exists()).toBe(true);
|
||||
expect(submitBtn.text()).toBe('Save integration');
|
||||
|
||||
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 } },
|
||||
{ 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');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findSubmitButton().exists()).toBe(true);
|
||||
expect(findSubmitButton().text()).toBe('Save integration');
|
||||
enableIntegration(0, integrationName);
|
||||
|
||||
const sampleMapping = { field: 'test' };
|
||||
findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
|
||||
findForm().trigger('submit');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
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 } },
|
||||
{
|
||||
type: typeSet.http,
|
||||
variables: {
|
||||
name: integrationName,
|
||||
active: true,
|
||||
payloadAttributeMappings: sampleMapping,
|
||||
payloadExample: null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows for update-integration with the correct form values for HTTP', async () => {
|
||||
it('update', () => {
|
||||
createComponent({
|
||||
data: {
|
||||
selectedIntegration: typeSet.http,
|
||||
|
@ -163,26 +166,44 @@ describe('AlertsSettingsFormNew', () => {
|
|||
loading: false,
|
||||
},
|
||||
});
|
||||
const updatedIntegrationName = 'Test integration post';
|
||||
enableIntegration(0, updatedIntegrationName);
|
||||
|
||||
await findFormFields().at(0).setValue('Test integration post');
|
||||
await findFormToggle().trigger('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findSubmitButton().exists()).toBe(true);
|
||||
expect(findSubmitButton().text()).toBe('Save integration');
|
||||
const submitBtn = findSubmitButton();
|
||||
expect(submitBtn.exists()).toBe(true);
|
||||
expect(submitBtn.text()).toBe('Save integration');
|
||||
|
||||
findForm().trigger('submit');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('update-integration')).toBeTruthy();
|
||||
expect(wrapper.emitted('update-integration')[0]).toEqual([
|
||||
{ type: typeSet.http, variables: { name: 'Test integration post', active: true } },
|
||||
{ type: typeSet.http, variables: { name: updatedIntegrationName, active: true } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PROMETHEUS', () => {
|
||||
it('create', async () => {
|
||||
createComponent();
|
||||
|
||||
await selectOptionAtIndex(2);
|
||||
|
||||
const apiUrl = 'https://test.com';
|
||||
enableIntegration(1, apiUrl);
|
||||
|
||||
findFormToggle().trigger('click');
|
||||
|
||||
const submitBtn = findSubmitButton();
|
||||
expect(submitBtn.exists()).toBe(true);
|
||||
expect(submitBtn.text()).toBe('Save integration');
|
||||
|
||||
findForm().trigger('submit');
|
||||
|
||||
expect(wrapper.emitted('create-new-integration')[0]).toEqual([
|
||||
{ type: typeSet.prometheus, variables: { apiUrl, active: true } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows for update-integration with the correct form values for PROMETHEUS', async () => {
|
||||
it('update', () => {
|
||||
createComponent({
|
||||
data: {
|
||||
selectedIntegration: typeSet.prometheus,
|
||||
|
@ -193,25 +214,21 @@ describe('AlertsSettingsFormNew', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await findFormFields().at(0).setValue('Test integration');
|
||||
await findFormFields().at(1).setValue('https://test-post.com');
|
||||
await findFormToggle().trigger('click');
|
||||
const apiUrl = 'https://test-post.com';
|
||||
enableIntegration(1, apiUrl);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findSubmitButton().exists()).toBe(true);
|
||||
expect(findSubmitButton().text()).toBe('Save integration');
|
||||
const submitBtn = findSubmitButton();
|
||||
expect(submitBtn.exists()).toBe(true);
|
||||
expect(submitBtn.text()).toBe('Save integration');
|
||||
|
||||
findForm().trigger('submit');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('update-integration')).toBeTruthy();
|
||||
expect(wrapper.emitted('update-integration')[0]).toEqual([
|
||||
{ type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } },
|
||||
{ type: typeSet.prometheus, variables: { apiUrl, active: true } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitting the integration with a JSON test payload', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue