Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d5cf5cf4f7
commit
4bc0e06402
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlDeprecatedButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import {
|
||||
RICH_BLOB_VIEWER,
|
||||
RICH_BLOB_VIEWER_TITLE,
|
||||
|
|
@ -11,7 +11,7 @@ export default {
|
|||
components: {
|
||||
GlIcon,
|
||||
GlButtonGroup,
|
||||
GlDeprecatedButton,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -46,7 +46,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<gl-button-group class="js-blob-viewer-switcher mx-2">
|
||||
<gl-deprecated-button
|
||||
<gl-button
|
||||
v-gl-tooltip.hover
|
||||
:aria-label="$options.SIMPLE_BLOB_VIEWER_TITLE"
|
||||
:title="$options.SIMPLE_BLOB_VIEWER_TITLE"
|
||||
|
|
@ -55,8 +55,8 @@ export default {
|
|||
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
|
||||
>
|
||||
<gl-icon name="code" :size="14" />
|
||||
</gl-deprecated-button>
|
||||
<gl-deprecated-button
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-gl-tooltip.hover
|
||||
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
|
||||
:title="$options.RICH_BLOB_VIEWER_TITLE"
|
||||
|
|
@ -65,6 +65,6 @@ export default {
|
|||
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
|
||||
>
|
||||
<gl-icon name="document" :size="14" />
|
||||
</gl-deprecated-button>
|
||||
</gl-button>
|
||||
</gl-button-group>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import eventHub from '../event_hub';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { GlFormGroup, GlToggle } from '@gitlab/ui';
|
||||
|
|
@ -21,6 +22,9 @@ export default {
|
|||
activated: this.initialActivated,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isInheriting']),
|
||||
},
|
||||
mounted() {
|
||||
// Initialize view
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -42,6 +46,7 @@ export default {
|
|||
v-model="activated"
|
||||
name="service[active]"
|
||||
class="gl-display-block gl-line-height-0"
|
||||
:disabled="isInheriting"
|
||||
@change="onToggle"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
|
@ -50,7 +55,12 @@ export default {
|
|||
<div class="form-group row" role="group">
|
||||
<label for="service[active]" class="col-form-label col-sm-2">{{ __('Active') }}</label>
|
||||
<div class="col-sm-10 pt-1">
|
||||
<gl-toggle v-model="activated" name="service[active]" @change="onToggle" />
|
||||
<gl-toggle
|
||||
v-model="activated"
|
||||
name="service[active]"
|
||||
:disabled="isInheriting"
|
||||
@change="onToggle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import eventHub from '../event_hub';
|
||||
import { capitalize, lowerCase, isEmpty } from 'lodash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
|
@ -59,6 +60,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isInheriting']),
|
||||
isCheckbox() {
|
||||
return this.type === 'checkbox';
|
||||
},
|
||||
|
|
@ -107,6 +109,7 @@ export default {
|
|||
id: this.fieldId,
|
||||
name: this.fieldName,
|
||||
state: this.valid,
|
||||
readonly: this.isInheriting,
|
||||
};
|
||||
},
|
||||
valid() {
|
||||
|
|
@ -142,12 +145,15 @@ export default {
|
|||
</template>
|
||||
|
||||
<template v-if="isCheckbox">
|
||||
<input :name="fieldName" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="model" v-bind="sharedProps">
|
||||
<input :name="fieldName" type="hidden" :value="model || false" />
|
||||
<gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheriting">
|
||||
{{ humanizedTitle }}
|
||||
</gl-form-checkbox>
|
||||
</template>
|
||||
<gl-form-select v-else-if="isSelect" v-model="model" v-bind="sharedProps" :options="options" />
|
||||
<template v-else-if="isSelect">
|
||||
<input type="hidden" :name="fieldName" :value="model" />
|
||||
<gl-form-select :id="fieldId" v-model="model" :options="options" :disabled="isInheriting" />
|
||||
</template>
|
||||
<gl-form-textarea
|
||||
v-else-if="isTextarea"
|
||||
v-model="model"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
import OverrideDropdown from './override_dropdown.vue';
|
||||
import ActiveToggle from './active_toggle.vue';
|
||||
import JiraTriggerFields from './jira_trigger_fields.vue';
|
||||
import JiraIssuesFields from './jira_issues_fields.vue';
|
||||
|
|
@ -9,6 +12,7 @@ import DynamicField from './dynamic_field.vue';
|
|||
export default {
|
||||
name: 'IntegrationForm',
|
||||
components: {
|
||||
OverrideDropdown,
|
||||
ActiveToggle,
|
||||
JiraTriggerFields,
|
||||
JiraIssuesFields,
|
||||
|
|
@ -16,55 +20,55 @@ export default {
|
|||
DynamicField,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
activeToggleProps: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
triggerFieldsProps: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
jiraIssuesProps: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
triggerEvents: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentKey', 'propsSource']),
|
||||
...mapState(['adminState', 'override']),
|
||||
isJira() {
|
||||
return this.type === 'jira';
|
||||
return this.propsSource.type === 'jira';
|
||||
},
|
||||
showJiraIssuesFields() {
|
||||
return this.isJira && this.glFeatures.jiraIssuesIntegration;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setOverride']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<active-toggle v-if="showActive" v-bind="activeToggleProps" />
|
||||
<jira-trigger-fields v-if="isJira" v-bind="triggerFieldsProps" />
|
||||
<trigger-fields v-else-if="triggerEvents.length" :events="triggerEvents" :type="type" />
|
||||
<dynamic-field v-for="field in fields" :key="field.name" v-bind="field" />
|
||||
<jira-issues-fields v-if="showJiraIssuesFields" v-bind="jiraIssuesProps" />
|
||||
<override-dropdown
|
||||
v-if="adminState !== null"
|
||||
:inherit-from-id="adminState.id"
|
||||
:override="override"
|
||||
@change="setOverride"
|
||||
/>
|
||||
<active-toggle
|
||||
v-if="propsSource.showActive"
|
||||
:key="`${currentKey}-active-toggle`"
|
||||
v-bind="propsSource.activeToggleProps"
|
||||
/>
|
||||
<jira-trigger-fields
|
||||
v-if="isJira"
|
||||
:key="`${currentKey}-jira-trigger-fields`"
|
||||
v-bind="propsSource.triggerFieldsProps"
|
||||
/>
|
||||
<trigger-fields
|
||||
v-else-if="propsSource.triggerEvents.length"
|
||||
:key="`${currentKey}-trigger-fields`"
|
||||
:events="propsSource.triggerEvents"
|
||||
:type="propsSource.type"
|
||||
/>
|
||||
<dynamic-field
|
||||
v-for="field in propsSource.fields"
|
||||
:key="`${currentKey}-${field.name}`"
|
||||
v-bind="field"
|
||||
/>
|
||||
<jira-issues-fields
|
||||
v-if="showJiraIssuesFields"
|
||||
:key="`${currentKey}-jira-issues-fields`"
|
||||
v-bind="propsSource.jiraIssuesProps"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ export default {
|
|||
}}
|
||||
</p>
|
||||
<template v-if="showJiraIssuesIntegration">
|
||||
<input name="service[issues_enabled]" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="enableJiraIssues" name="service[issues_enabled]">
|
||||
<input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
|
||||
<gl-form-checkbox v-model="enableJiraIssues">
|
||||
{{ s__('JiraService|Enable Jira issues') }}
|
||||
<template #help>
|
||||
{{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
import { GlFormGroup, GlFormCheckbox, GlFormRadio } from '@gitlab/ui';
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isInheriting']),
|
||||
showEnableComments() {
|
||||
return this.triggerCommit || this.triggerMergeRequest;
|
||||
},
|
||||
|
|
@ -73,13 +75,17 @@ export default {
|
|||
)
|
||||
"
|
||||
>
|
||||
<input name="service[commit_events]" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="triggerCommit" name="service[commit_events]">
|
||||
<input name="service[commit_events]" type="hidden" :value="triggerCommit || false" />
|
||||
<gl-form-checkbox v-model="triggerCommit" :disabled="isInheriting">
|
||||
{{ __('Commit') }}
|
||||
</gl-form-checkbox>
|
||||
|
||||
<input name="service[merge_requests_events]" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="triggerMergeRequest" name="service[merge_requests_events]">
|
||||
<input
|
||||
name="service[merge_requests_events]"
|
||||
type="hidden"
|
||||
:value="triggerMergeRequest || false"
|
||||
/>
|
||||
<gl-form-checkbox v-model="triggerMergeRequest" :disabled="isInheriting">
|
||||
{{ __('Merge request') }}
|
||||
</gl-form-checkbox>
|
||||
</gl-form-group>
|
||||
|
|
@ -89,8 +95,12 @@ export default {
|
|||
:label="s__('Integrations|Comment settings:')"
|
||||
data-testid="comment-settings"
|
||||
>
|
||||
<input name="service[comment_on_event_enabled]" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="enableComments" name="service[comment_on_event_enabled]">
|
||||
<input
|
||||
name="service[comment_on_event_enabled]"
|
||||
type="hidden"
|
||||
:value="enableComments || false"
|
||||
/>
|
||||
<gl-form-checkbox v-model="enableComments" :disabled="isInheriting">
|
||||
{{ s__('Integrations|Enable comments') }}
|
||||
</gl-form-checkbox>
|
||||
</gl-form-group>
|
||||
|
|
@ -100,12 +110,18 @@ export default {
|
|||
:label="s__('Integrations|Comment detail:')"
|
||||
data-testid="comment-detail"
|
||||
>
|
||||
<input
|
||||
v-if="isInheriting"
|
||||
name="service[comment_detail]"
|
||||
type="hidden"
|
||||
:value="commentDetail"
|
||||
/>
|
||||
<gl-form-radio
|
||||
v-for="commentDetailOption in commentDetailOptions"
|
||||
:key="commentDetailOption.value"
|
||||
v-model="commentDetail"
|
||||
:value="commentDetailOption.value"
|
||||
name="service[comment_detail]"
|
||||
:disabled="isInheriting"
|
||||
>
|
||||
{{ commentDetailOption.label }}
|
||||
<template #help>
|
||||
|
|
@ -126,13 +142,17 @@ export default {
|
|||
}}
|
||||
</label>
|
||||
|
||||
<input name="service[commit_events]" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="triggerCommit" name="service[commit_events]">
|
||||
<input name="service[commit_events]" type="hidden" :value="triggerCommit || false" />
|
||||
<gl-form-checkbox v-model="triggerCommit" :disabled="isInheriting">
|
||||
{{ __('Commit') }}
|
||||
</gl-form-checkbox>
|
||||
|
||||
<input name="service[merge_requests_events]" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="triggerMergeRequest" name="service[merge_requests_events]">
|
||||
<input
|
||||
name="service[merge_requests_events]"
|
||||
type="hidden"
|
||||
:value="triggerMergeRequest || false"
|
||||
/>
|
||||
<gl-form-checkbox v-model="triggerMergeRequest" :disabled="isInheriting">
|
||||
{{ __('Merge request') }}
|
||||
</gl-form-checkbox>
|
||||
|
||||
|
|
@ -144,8 +164,12 @@ export default {
|
|||
<label>
|
||||
{{ s__('Integrations|Comment settings:') }}
|
||||
</label>
|
||||
<input name="service[comment_on_event_enabled]" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="enableComments" name="service[comment_on_event_enabled]">
|
||||
<input
|
||||
name="service[comment_on_event_enabled]"
|
||||
type="hidden"
|
||||
:value="enableComments || false"
|
||||
/>
|
||||
<gl-form-checkbox v-model="enableComments" :disabled="isInheriting">
|
||||
{{ s__('Integrations|Enable comments') }}
|
||||
</gl-form-checkbox>
|
||||
|
||||
|
|
@ -153,12 +177,18 @@ export default {
|
|||
<label>
|
||||
{{ s__('Integrations|Comment detail:') }}
|
||||
</label>
|
||||
<input
|
||||
v-if="isInheriting"
|
||||
name="service[comment_detail]"
|
||||
type="hidden"
|
||||
:value="commentDetail"
|
||||
/>
|
||||
<gl-form-radio
|
||||
v-for="commentDetailOption in commentDetailOptions"
|
||||
:key="commentDetailOption.value"
|
||||
v-model="commentDetail"
|
||||
:value="commentDetailOption.value"
|
||||
name="service[comment_detail]"
|
||||
:disabled="isInheriting"
|
||||
>
|
||||
{{ commentDetailOption.label }}
|
||||
<template #help>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import { s__ } from '~/locale';
|
||||
import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
|
||||
|
||||
const dropdownOptions = [
|
||||
{
|
||||
value: false,
|
||||
text: s__('Integrations|Use instance level settings'),
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
text: s__('Integrations|Use custom settings'),
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
dropdownOptions,
|
||||
name: 'OverrideDropdown',
|
||||
components: {
|
||||
GlNewDropdown,
|
||||
GlNewDropdownItem,
|
||||
},
|
||||
props: {
|
||||
inheritFromId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
override: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: dropdownOptions.find(x => x.value === this.override),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClick(option) {
|
||||
this.selected = option;
|
||||
this.$emit('change', option.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-py-4 gl-mt-5 gl-mb-6 gl-border-t-1 gl-border-t-solid gl-border-b-1 gl-border-b-solid gl-border-gray-100"
|
||||
>
|
||||
<span>{{ s__('Integrations|This integration has multiple settings available.') }}</span>
|
||||
<input name="service[inherit_from_id]" :value="override ? '' : inheritFromId" type="hidden" />
|
||||
<gl-new-dropdown :text="selected.text">
|
||||
<gl-new-dropdown-item
|
||||
v-for="option in $options.dropdownOptions"
|
||||
:key="option.value"
|
||||
@click="onClick(option)"
|
||||
>
|
||||
{{ option.text }}
|
||||
</gl-new-dropdown-item>
|
||||
</gl-new-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { startCase } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
|
||||
|
|
@ -32,6 +33,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isInheriting']),
|
||||
placeholder() {
|
||||
return placeholderForType[this.type];
|
||||
},
|
||||
|
|
@ -57,8 +59,8 @@ export default {
|
|||
>
|
||||
<div id="trigger-fields" class="gl-pt-3">
|
||||
<gl-form-group v-for="event in events" :key="event.title" :description="event.description">
|
||||
<input :name="checkboxName(event.name)" type="hidden" value="false" />
|
||||
<gl-form-checkbox v-model="event.value" :name="checkboxName(event.name)">
|
||||
<input :name="checkboxName(event.name)" type="hidden" :value="event.value || false" />
|
||||
<gl-form-checkbox v-model="event.value" :disabled="isInheriting">
|
||||
{{ startCase(event.title) }}
|
||||
</gl-form-checkbox>
|
||||
<gl-form-input
|
||||
|
|
@ -66,6 +68,7 @@ export default {
|
|||
v-model="event.field.value"
|
||||
:name="fieldName(event.field.name)"
|
||||
:placeholder="placeholder"
|
||||
:readonly="isInheriting"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import Vue from 'vue';
|
||||
import { createStore } from './store';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import IntegrationForm from './components/integration_form.vue';
|
||||
|
||||
export default el => {
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseBooleanInData(data) {
|
||||
const result = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
result[key] = parseBoolean(value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
function parseBooleanInData(data) {
|
||||
const result = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
result[key] = parseBoolean(value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseDatasetToProps(data) {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
commentDetail,
|
||||
projectKey,
|
||||
|
|
@ -23,8 +21,9 @@ export default el => {
|
|||
editProjectPath,
|
||||
triggerEvents,
|
||||
fields,
|
||||
inheritFromId,
|
||||
...booleanAttributes
|
||||
} = el.dataset;
|
||||
} = data;
|
||||
const {
|
||||
showActive,
|
||||
activated,
|
||||
|
|
@ -35,33 +34,53 @@ export default el => {
|
|||
enableJiraIssues,
|
||||
} = parseBooleanInData(booleanAttributes);
|
||||
|
||||
return {
|
||||
activeToggleProps: {
|
||||
initialActivated: activated,
|
||||
},
|
||||
showActive,
|
||||
type,
|
||||
triggerFieldsProps: {
|
||||
initialTriggerCommit: commitEvents,
|
||||
initialTriggerMergeRequest: mergeRequestEvents,
|
||||
initialEnableComments: enableComments,
|
||||
initialCommentDetail: commentDetail,
|
||||
},
|
||||
jiraIssuesProps: {
|
||||
showJiraIssuesIntegration,
|
||||
initialEnableJiraIssues: enableJiraIssues,
|
||||
initialProjectKey: projectKey,
|
||||
upgradePlanPath,
|
||||
editProjectPath,
|
||||
},
|
||||
triggerEvents: JSON.parse(triggerEvents),
|
||||
fields: JSON.parse(fields),
|
||||
inheritFromId: parseInt(inheritFromId, 10),
|
||||
id: parseInt(id, 10),
|
||||
};
|
||||
}
|
||||
|
||||
export default (el, adminEl) => {
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const props = parseDatasetToProps(el.dataset);
|
||||
|
||||
const initialState = {
|
||||
adminState: null,
|
||||
customState: props,
|
||||
};
|
||||
|
||||
if (adminEl) {
|
||||
initialState.adminState = Object.freeze(parseDatasetToProps(adminEl.dataset));
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store: createStore(initialState),
|
||||
render(createElement) {
|
||||
return createElement(IntegrationForm, {
|
||||
props: {
|
||||
activeToggleProps: {
|
||||
initialActivated: activated,
|
||||
},
|
||||
showActive,
|
||||
type,
|
||||
triggerFieldsProps: {
|
||||
initialTriggerCommit: commitEvents,
|
||||
initialTriggerMergeRequest: mergeRequestEvents,
|
||||
initialEnableComments: enableComments,
|
||||
initialCommentDetail: commentDetail,
|
||||
},
|
||||
jiraIssuesProps: {
|
||||
showJiraIssuesIntegration,
|
||||
initialEnableJiraIssues: enableJiraIssues,
|
||||
initialProjectKey: projectKey,
|
||||
upgradePlanPath,
|
||||
editProjectPath,
|
||||
},
|
||||
triggerEvents: JSON.parse(triggerEvents),
|
||||
fields: JSON.parse(fields),
|
||||
},
|
||||
});
|
||||
return createElement(IntegrationForm);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const isInheriting = state => (state.adminState === null ? false : !state.override);
|
||||
|
||||
export const propsSource = (state, getters) =>
|
||||
getters.isInheriting ? state.adminState : state.customState;
|
||||
|
||||
export const currentKey = (state, getters) => (getters.isInheriting ? 'admin' : 'custom');
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
import createState from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const createStore = (initialState = {}) =>
|
||||
new Vuex.Store({
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
state: createState(initialState),
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const SET_OVERRIDE = 'SET_OVERRIDE';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_OVERRIDE](state, override) {
|
||||
state.override = override;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export default ({ adminState = null, customState = {} } = {}) => {
|
||||
const override = adminState !== null ? adminState.id !== customState.inheritFromId : false;
|
||||
|
||||
return {
|
||||
override,
|
||||
adminState,
|
||||
customState,
|
||||
};
|
||||
};
|
||||
|
|
@ -22,7 +22,10 @@ export default class IntegrationSettingsForm {
|
|||
|
||||
init() {
|
||||
// Init Vue component
|
||||
initForm(document.querySelector('.js-vue-integration-settings'));
|
||||
initForm(
|
||||
document.querySelector('.js-vue-integration-settings'),
|
||||
document.querySelector('.js-vue-admin-integration-settings'),
|
||||
);
|
||||
eventHub.$on('toggle', active => {
|
||||
this.formActive = active;
|
||||
this.handleServiceToggle();
|
||||
|
|
|
|||
|
|
@ -303,40 +303,7 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
|
|||
});
|
||||
}
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export function keypressNoteText(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
return;
|
||||
}
|
||||
const keys = {
|
||||
'*': '**{text}**', // wraps with bold character
|
||||
_: '_{text}_', // wraps with italic character
|
||||
'`': '`{text}`', // wraps with inline character
|
||||
"'": "'{text}'", // single quotes
|
||||
'"': '"{text}"', // double quotes
|
||||
'[': '[{text}]', // brackets
|
||||
'{': '{{text}}', // braces
|
||||
'(': '({text})', // parentheses
|
||||
'<': '<{text}>', // angle brackets
|
||||
};
|
||||
const tag = keys[e.key];
|
||||
|
||||
if (tag) {
|
||||
updateText({
|
||||
tag,
|
||||
textArea: this,
|
||||
blockTag: '',
|
||||
wrap: true,
|
||||
select: '',
|
||||
tagContent: '',
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
|
||||
export function addMarkdownListeners(form) {
|
||||
$('.markdown-area').on('keydown', keypressNoteText);
|
||||
return $('.js-md', form)
|
||||
.off('click')
|
||||
.on('click', function() {
|
||||
|
|
@ -373,6 +340,5 @@ export function addEditorMarkdownListeners(editor) {
|
|||
}
|
||||
|
||||
export function removeMarkdownListeners(form) {
|
||||
$('.markdown-area').off('keydown');
|
||||
return $('.js-md', form).off('click');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
/**
|
||||
* Renders Code quality body text
|
||||
* Fixed: [name] in [link]:[line]
|
||||
*/
|
||||
import ReportLink from '~/reports/components/report_link.vue';
|
||||
import { STATUS_SUCCESS } from '~/reports/constants';
|
||||
|
||||
export default {
|
||||
name: 'CodequalityIssueBody',
|
||||
|
||||
components: {
|
||||
ReportLink,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isStatusSuccess() {
|
||||
return this.status === STATUS_SUCCESS;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="report-block-list-issue-description gl-mt-2 gl-mb-2">
|
||||
<div class="report-block-list-issue-description-text">
|
||||
<template v-if="isStatusSuccess">{{ s__('ciReport|Fixed:') }}</template>
|
||||
|
||||
{{ issue.name }}
|
||||
</div>
|
||||
|
||||
<report-link v-if="issue.path" :issue="issue" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { componentNames } from '~/reports/components/issue_body';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import ReportSection from '~/reports/components/report_section.vue';
|
||||
import createStore from './store';
|
||||
|
||||
export default {
|
||||
name: 'GroupedCodequalityReportsApp',
|
||||
store: createStore(),
|
||||
components: {
|
||||
ReportSection,
|
||||
},
|
||||
props: {
|
||||
headPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headBlobPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
baseBlobPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
codequalityHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
componentNames,
|
||||
computed: {
|
||||
...mapState(['newIssues', 'resolvedIssues']),
|
||||
...mapGetters([
|
||||
'hasCodequalityIssues',
|
||||
'codequalityStatus',
|
||||
'codequalityText',
|
||||
'codequalityPopover',
|
||||
]),
|
||||
},
|
||||
created() {
|
||||
this.setPaths({
|
||||
basePath: this.basePath,
|
||||
headPath: this.headPath,
|
||||
baseBlobPath: this.baseBlobPath,
|
||||
headBlobPath: this.headBlobPath,
|
||||
helpPath: this.codequalityHelpPath,
|
||||
});
|
||||
|
||||
this.fetchReports();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchReports', 'setPaths']),
|
||||
},
|
||||
loadingText: sprintf(s__('ciReport|Loading %{reportName} report'), {
|
||||
reportName: 'codeclimate',
|
||||
}),
|
||||
errorText: sprintf(s__('ciReport|Failed to load %{reportName} report'), {
|
||||
reportName: 'codeclimate',
|
||||
}),
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<report-section
|
||||
:status="codequalityStatus"
|
||||
:loading-text="$options.loadingText"
|
||||
:error-text="$options.errorText"
|
||||
:success-text="codequalityText"
|
||||
:unresolved-issues="newIssues"
|
||||
:resolved-issues="resolvedIssues"
|
||||
:has-issues="hasCodequalityIssues"
|
||||
:component="$options.componentNames.CodequalityIssueBody"
|
||||
:popover-options="codequalityPopover"
|
||||
class="js-codequality-widget mr-widget-border-top mr-report"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
import TestIssueBody from './test_issue_body.vue';
|
||||
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
|
||||
import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue';
|
||||
|
||||
export const components = {
|
||||
AccessibilityIssueBody,
|
||||
CodequalityIssueBody,
|
||||
TestIssueBody,
|
||||
};
|
||||
|
||||
export const componentNames = {
|
||||
AccessibilityIssueBody: AccessibilityIssueBody.name,
|
||||
CodequalityIssueBody: CodequalityIssueBody.name,
|
||||
TestIssueBody: TestIssueBody.name,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import eventHub from './event_hub';
|
|||
import notify from '~/lib/utils/notify';
|
||||
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
|
||||
import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue';
|
||||
import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
|
||||
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
|
||||
import { setFaviconOverlay } from '../lib/utils/common_utils';
|
||||
import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
|
||||
|
|
@ -75,6 +76,7 @@ export default {
|
|||
'mr-widget-auto-merge-failed': AutoMergeFailed,
|
||||
'mr-widget-rebase': RebaseState,
|
||||
SourceBranchRemovalStatus,
|
||||
GroupedCodequalityReportsApp,
|
||||
GroupedTestReportsApp,
|
||||
TerraformPlan,
|
||||
GroupedAccessibilityReportsApp,
|
||||
|
|
@ -111,6 +113,9 @@ export default {
|
|||
shouldSuggestPipelines() {
|
||||
return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath;
|
||||
},
|
||||
shouldRenderCodeQuality() {
|
||||
return this.mr?.codeclimate?.head_path;
|
||||
},
|
||||
shouldRenderRelatedLinks() {
|
||||
return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
|
||||
},
|
||||
|
|
@ -380,6 +385,15 @@ export default {
|
|||
:mr="mr"
|
||||
/>
|
||||
<div class="mr-section-container mr-widget-workflow">
|
||||
<grouped-codequality-reports-app
|
||||
v-if="shouldRenderCodeQuality"
|
||||
:base-path="mr.codeclimate.base_path"
|
||||
:head-path="mr.codeclimate.head_path"
|
||||
:head-blob-path="mr.headBlobPath"
|
||||
:base-blob-path="mr.baseBlobPath"
|
||||
:codequality-help-path="mr.codequalityHelpPath"
|
||||
/>
|
||||
|
||||
<grouped-test-reports-app
|
||||
v-if="mr.testResultsPath"
|
||||
class="js-reports-container"
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ export default function deviseState(data) {
|
|||
return stateKey.checking;
|
||||
} else if (data.has_conflicts) {
|
||||
return stateKey.conflicts;
|
||||
} else if (data.work_in_progress) {
|
||||
return stateKey.workInProgress;
|
||||
} else if (this.shouldBeRebased) {
|
||||
return stateKey.rebase;
|
||||
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
|
||||
return stateKey.pipelineFailed;
|
||||
} else if (data.work_in_progress) {
|
||||
return stateKey.workInProgress;
|
||||
} else if (this.hasMergeableDiscussionsState) {
|
||||
return stateKey.unresolvedDiscussions;
|
||||
} else if (this.isPipelineBlocked) {
|
||||
|
|
|
|||
|
|
@ -185,6 +185,13 @@ export default class MergeRequestStore {
|
|||
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
|
||||
this.humanAccess = data.human_access;
|
||||
this.newPipelinePath = data.new_project_pipeline_path;
|
||||
|
||||
// codeclimate
|
||||
const blobPath = data.blob_path || {};
|
||||
this.headBlobPath = blobPath.head_path || '';
|
||||
this.baseBlobPath = blobPath.base_path || '';
|
||||
this.codequalityHelpPath = data.codequality_help_path;
|
||||
this.codeclimate = data.codeclimate;
|
||||
}
|
||||
|
||||
get isNothingToMergeState() {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ module ServiceParams
|
|||
:external_wiki_url,
|
||||
:google_iap_service_account_json,
|
||||
:google_iap_audience_client_id,
|
||||
:inherit_from_id,
|
||||
# We're using `issues_events` and `merge_requests_events`
|
||||
# in the view so we still need to explicitly state them
|
||||
# here. `Service#event_names` would only give
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@ class Projects::ServicesController < Projects::ApplicationController
|
|||
layout "project_settings"
|
||||
|
||||
def edit
|
||||
@admin_integration = Service.instance_for(service.type)
|
||||
end
|
||||
|
||||
def update
|
||||
@service.attributes = service_params[:service]
|
||||
@service.inherit_from_id = nil if service_params[:service][:inherit_from_id].blank?
|
||||
|
||||
saved = @service.save(context: :manual_change)
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ module ServicesHelper
|
|||
|
||||
def integration_form_data(integration)
|
||||
{
|
||||
id: integration.id,
|
||||
show_active: integration.show_active_box?.to_s,
|
||||
activated: (integration.active || integration.new_record?).to_s,
|
||||
type: integration.to_param,
|
||||
|
|
@ -109,7 +110,8 @@ module ServicesHelper
|
|||
enable_comments: integration.comment_on_event_enabled.to_s,
|
||||
comment_detail: integration.comment_detail,
|
||||
trigger_events: trigger_events_for_service(integration),
|
||||
fields: fields_for_service(integration)
|
||||
fields: fields_for_service(integration),
|
||||
inherit_from_id: integration.inherit_from_id
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -364,6 +364,10 @@ class Service < ApplicationRecord
|
|||
exists?(instance: true, type: type)
|
||||
end
|
||||
|
||||
def self.instance_for(type)
|
||||
find_by(instance: true, type: type)
|
||||
end
|
||||
|
||||
# override if needed
|
||||
def supports_data_fields?
|
||||
false
|
||||
|
|
|
|||
|
|
@ -148,9 +148,7 @@
|
|||
%a.muted{ href: user_url(assignee), style: "color: #333333; text-decoration: none; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; vertical-align: top;" }
|
||||
= assignee.name
|
||||
|
||||
-# EE-specific start
|
||||
= render 'layouts/mailer/additional_text'
|
||||
-# EE-specific end
|
||||
= render_if_exists 'layouts/mailer/additional_text'
|
||||
|
||||
%tr.footer
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@
|
|||
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}';
|
||||
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
|
||||
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
|
||||
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
|
||||
|
||||
#js-vue-mr-widget.mr-widget
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
= markdown integration.help
|
||||
|
||||
.service-settings
|
||||
- if @admin_integration
|
||||
.js-vue-admin-integration-settings{ data: integration_form_data(@admin_integration) }
|
||||
.js-vue-integration-settings{ data: integration_form_data(integration) }
|
||||
|
||||
- if show_service_trigger_events?(integration)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ module ApplicationWorker
|
|||
include Sidekiq::Worker # rubocop:disable Cop/IncludeSidekiqWorker
|
||||
include WorkerAttributes
|
||||
include WorkerContext
|
||||
include Gitlab::SidekiqVersioning::Worker
|
||||
|
||||
LOGGING_EXTRA_KEY = 'extra'
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add override selector for project-level integrations
|
||||
merge_request: 34742
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Surround selected text in markdown fields on certain key presses
|
||||
merge_request: 25748
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use new vuex store for code quality MR widget
|
||||
merge_request: 36120
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Check WIP status after all other possible statuses
|
||||
merge_request: 36624
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -239,7 +239,7 @@ The following documentation relates to the DevOps **Verify** stage:
|
|||
|
||||
| Verify topics | Description |
|
||||
|:----------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------|
|
||||
| [Code Quality reports](user/project/merge_requests/code_quality.md) **(STARTER)** | Analyze source code quality. |
|
||||
| [Code Quality reports](user/project/merge_requests/code_quality.md) | Analyze source code quality. |
|
||||
| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. |
|
||||
| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. |
|
||||
| [Multi-project pipelines](ci/multi_project_pipelines.md) **(PREMIUM)** | Visualize entire pipelines that span multiple projects, including all cross-project inter-dependencies. |
|
||||
|
|
|
|||
|
|
@ -9884,6 +9884,11 @@ type Project {
|
|||
"""
|
||||
sastCiConfiguration: SastCiConfiguration
|
||||
|
||||
"""
|
||||
Information about security analyzers used in the project
|
||||
"""
|
||||
securityScanners: SecurityScanners
|
||||
|
||||
"""
|
||||
Detailed version of a Sentry error on the project
|
||||
"""
|
||||
|
|
@ -12053,6 +12058,37 @@ type SecurityReportSummarySection {
|
|||
vulnerabilitiesCount: Int
|
||||
}
|
||||
|
||||
"""
|
||||
The type of the security scanner.
|
||||
"""
|
||||
enum SecurityScannerType {
|
||||
CONTAINER_SCANNING
|
||||
DAST
|
||||
DEPENDENCY_SCANNING
|
||||
SAST
|
||||
SECRET_DETECTION
|
||||
}
|
||||
|
||||
"""
|
||||
Represents a list of security scanners
|
||||
"""
|
||||
type SecurityScanners {
|
||||
"""
|
||||
List of analyzers which are available for the project.
|
||||
"""
|
||||
available: [SecurityScannerType!]
|
||||
|
||||
"""
|
||||
List of analyzers which are enabled for the project.
|
||||
"""
|
||||
enabled: [SecurityScannerType!]
|
||||
|
||||
"""
|
||||
List of analyzers which ran successfully in the latest pipeline.
|
||||
"""
|
||||
pipelineRun: [SecurityScannerType!]
|
||||
}
|
||||
|
||||
"""
|
||||
A Sentry error.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -29080,6 +29080,20 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "securityScanners",
|
||||
"description": "Information about security analyzers used in the project",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "SecurityScanners",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "sentryDetailedError",
|
||||
"description": "Detailed version of a Sentry error on the project",
|
||||
|
|
@ -35291,6 +35305,126 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "SecurityScannerType",
|
||||
"description": "The type of the security scanner.",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "SAST",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "DAST",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "DEPENDENCY_SCANNING",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "CONTAINER_SCANNING",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "SECRET_DETECTION",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SecurityScanners",
|
||||
"description": "Represents a list of security scanners",
|
||||
"fields": [
|
||||
{
|
||||
"name": "available",
|
||||
"description": "List of analyzers which are available for the project.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "SecurityScannerType",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"description": "List of analyzers which are enabled for the project.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "SecurityScannerType",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pipelineRun",
|
||||
"description": "List of analyzers which ran successfully in the latest pipeline.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "SecurityScannerType",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SentryDetailedError",
|
||||
|
|
|
|||
|
|
@ -1434,6 +1434,7 @@ Information about pagination in a connection.
|
|||
| `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. |
|
||||
| `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state |
|
||||
| `sastCiConfiguration` | SastCiConfiguration | SAST CI configuration for the project |
|
||||
| `securityScanners` | SecurityScanners | Information about security analyzers used in the project |
|
||||
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
|
||||
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
|
||||
| `serviceDeskAddress` | String | E-mail address of the service desk. |
|
||||
|
|
@ -1746,6 +1747,16 @@ Represents a section of a summary of a security report
|
|||
| `scannedResourcesCsvPath` | String | Path to download all the scanned resources in CSV format |
|
||||
| `vulnerabilitiesCount` | Int | Total number of vulnerabilities |
|
||||
|
||||
## SecurityScanners
|
||||
|
||||
Represents a list of security scanners
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `available` | SecurityScannerType! => Array | List of analyzers which are available for the project. |
|
||||
| `enabled` | SecurityScannerType! => Array | List of analyzers which are enabled for the project. |
|
||||
| `pipelineRun` | SecurityScannerType! => Array | List of analyzers which ran successfully in the latest pipeline. |
|
||||
|
||||
## SentryDetailedError
|
||||
|
||||
A Sentry error.
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ Its feature set is listed on the table below according to DevOps stages.
|
|||
| [Browser Performance Testing](../user/project/merge_requests/browser_performance_testing.md) | Quickly determine the browser performance impact of pending code changes. |
|
||||
| [Load Performance Testing](../user/project/merge_requests/load_performance_testing.md) | Quickly determine the server performance impact of pending code changes. |
|
||||
| [CI services](services/README.md) | Link Docker containers with your base image.|
|
||||
| [Code Quality](../user/project/merge_requests/code_quality.md) **(STARTER)** | Analyze your source code quality. |
|
||||
| [Code Quality](../user/project/merge_requests/code_quality.md) | Analyze your source code quality. |
|
||||
| [GitLab CI/CD for external repositories](ci_cd_for_external_repos/index.md) **(PREMIUM)** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and Bitbucket Cloud. |
|
||||
| [Interactive Web Terminals](interactive_web_terminal/index.md) **(CORE ONLY)** | Open an interactive web terminal to debug the running jobs. |
|
||||
| [JUnit tests](junit_test_reports.md) | Identify script failures directly on merge requests. |
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ according to each stage (Verify, Package, Release).
|
|||
|
||||
1. **Verify**:
|
||||
- Automatically build and test your application with Continuous Integration.
|
||||
- Analyze your source code quality with [GitLab Code Quality](../../user/project/merge_requests/code_quality.md). **(STARTER)**
|
||||
- Analyze your source code quality with [GitLab Code Quality](../../user/project/merge_requests/code_quality.md).
|
||||
- Determine the browser performance impact of code changes with [Browser Performance Testing](../../user/project/merge_requests/browser_performance_testing.md). **(PREMIUM)**
|
||||
- Determine the server performance impact of code changes with [Load Performance Testing](../../user/project/merge_requests/load_performance_testing.md). **(PREMIUM)**
|
||||
- Perform a series of tests, such as [Container Scanning](../../user/application_security/container_scanning/index.md) **(ULTIMATE)**, [Dependency Scanning](../../user/application_security/dependency_scanning/index.md) **(ULTIMATE)**, and [JUnit tests](../junit_test_reports.md).
|
||||
|
|
|
|||
|
|
@ -146,9 +146,10 @@ plan report will be uploaded to GitLab as an artifact and will be automatically
|
|||
in merge requests. For more information, see
|
||||
[Output `terraform plan` information into a merge request](../../user/infrastructure/index.md#output-terraform-plan-information-into-a-merge-request).
|
||||
|
||||
#### `artifacts:reports:codequality` **(STARTER)**
|
||||
#### `artifacts:reports:codequality`
|
||||
|
||||
> - Introduced in GitLab 11.5.
|
||||
> - Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5.
|
||||
> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) in GitLab 13.2.
|
||||
> - Requires GitLab Runner 11.5 and above.
|
||||
|
||||
The `codequality` report collects [CodeQuality issues](../../user/project/merge_requests/code_quality.md)
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ The following table lists available parameters for jobs:
|
|||
| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0), `artifacts:reports:performance`, `artifacts:reports:load_performance`, and `artifacts:reports:metrics`. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:codequality`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0), `artifacts:reports:performance`, `artifacts:reports:load_performance`, and `artifacts:reports:metrics`. |
|
||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
||||
|
|
@ -3147,7 +3147,7 @@ These are the available report types:
|
|||
| [`artifacts:reports:dotenv`](../pipelines/job_artifacts.md#artifactsreportsdotenv) | The `dotenv` report collects a set of environment variables. |
|
||||
| [`artifacts:reports:cobertura`](../pipelines/job_artifacts.md#artifactsreportscobertura) | The `cobertura` report collects Cobertura coverage XML files. |
|
||||
| [`artifacts:reports:terraform`](../pipelines/job_artifacts.md#artifactsreportsterraform) | The `terraform` report collects Terraform `tfplan.json` files. |
|
||||
| [`artifacts:reports:codequality`](../pipelines/job_artifacts.md#artifactsreportscodequality-starter) **(STARTER)** | The `codequality` report collects CodeQuality issues. |
|
||||
| [`artifacts:reports:codequality`](../pipelines/job_artifacts.md#artifactsreportscodequality) | The `codequality` report collects CodeQuality issues. |
|
||||
| [`artifacts:reports:sast`](../pipelines/job_artifacts.md#artifactsreportssast-ultimate) **(ULTIMATE)** | The `sast` report collects Static Application Security Testing vulnerabilities. |
|
||||
| [`artifacts:reports:dependency_scanning`](../pipelines/job_artifacts.md#artifactsreportsdependency_scanning-ultimate) **(ULTIMATE)** | The `dependency_scanning` report collects Dependency Scanning vulnerabilities. |
|
||||
| [`artifacts:reports:container_scanning`](../pipelines/job_artifacts.md#artifactsreportscontainer_scanning-ultimate) **(ULTIMATE)** | The `container_scanning` report collects Container Scanning vulnerabilities. |
|
||||
|
|
|
|||
|
|
@ -16,6 +16,22 @@ Global navigation (the left-most pane in our three pane documentation) provides:
|
|||
- The ability to refine landing pages, so they don't have to do all the work of surfacing
|
||||
every page contained within the documentation.
|
||||
|
||||
## Quick start
|
||||
|
||||
To add a topic to the global nav, go to the directory that contains
|
||||
[navigation files](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/content/_data/)
|
||||
and edit the `yaml` file for your product area. You can copy an existing nav entry and
|
||||
edit it to point to your topic.
|
||||
|
||||
The files are:
|
||||
|
||||
| File | Document | Location |
|
||||
|-----------------------|--------------------------------------------------------------------|-------------------------------------------------------|
|
||||
| `charts-nav.yaml` | GitLab cloud native Helm Chart | `https://docs.gitlab.com/charts/` |
|
||||
| `default-nav.yaml` | GitLab Docs | `https://docs.gitlab.com/ee/` |
|
||||
| `omnibus-nav.yaml` | Omnibus GitLab Docs | `https://docs.gitlab.com/omnibus/` |
|
||||
| `runner-nav.yaml` | GitLab Runner Docs | `https://docs.gitlab.com/runner/` |
|
||||
|
||||
## Adding new items
|
||||
|
||||
All new pages need a new navigation item. Without a navigation, the page becomes "orphaned". That
|
||||
|
|
|
|||
|
|
@ -1346,6 +1346,11 @@ Tagged and released versions of GitLab documentation are available:
|
|||
The version introducing a new feature is added to the top of the topic in the documentation to provide
|
||||
a helpful link back to how the feature was developed.
|
||||
|
||||
TIP: **Tip:**
|
||||
Whenever you have documentation related to the `gitlab.rb` file, you're working with a self-managed installation.
|
||||
The section or page is therefore likely to apply only to self-managed instances.
|
||||
If so, the relevant "`TIER` ONLY" [Product badge](#product-badges) should be included at the highest applicable heading level.
|
||||
|
||||
### Text for documentation requiring version text
|
||||
|
||||
- For features that need to declare the GitLab version that the feature was introduced. Text similar
|
||||
|
|
|
|||
|
|
@ -64,6 +64,36 @@ the extra jobs will take resources away from jobs from workers that were already
|
|||
there, if the resources available to the Sidekiq process handling the namespace
|
||||
are not adjusted appropriately.
|
||||
|
||||
## Versioning
|
||||
|
||||
Version can be specified on each Sidekiq worker class.
|
||||
This is then sent along when the job is created.
|
||||
|
||||
```ruby
|
||||
class FooWorker
|
||||
include ApplicationWorker
|
||||
|
||||
version 2
|
||||
|
||||
def perform(*args)
|
||||
if job_version == 2
|
||||
foo = args.first['foo']
|
||||
else
|
||||
foo = args.first
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Under this schema, any worker is expected to be able to handle any job that was
|
||||
enqueued by an older version of that worker. This means that when changing the
|
||||
arguments a worker takes, you must increment the `version` (or set `version 1`
|
||||
if this is the first time a worker's arguments are changing), but also make sure
|
||||
that the worker is still able to handle jobs that were queued with any earlier
|
||||
version of the arguments. From the worker's `perform` method, you can read
|
||||
`self.job_version` if you want to specifically branch on job version, or you
|
||||
can read the number or type of provided arguments.
|
||||
|
||||
## Idempotent Jobs
|
||||
|
||||
It's known that a job can fail for multiple reasons. For example, network outages or bugs.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
type: reference, howto
|
||||
---
|
||||
|
||||
# Code Quality **(STARTER)**
|
||||
# Code Quality
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1984) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
|
||||
|
||||
|
|
@ -25,6 +25,11 @@ Code Quality:
|
|||
DevOps](../../../topics/autodevops/stages.md#auto-code-quality-starter).
|
||||
- Can be extended through [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) or a [custom tool](#implementing-a-custom-tool).
|
||||
|
||||
## Code Quality Widget
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1984) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
|
||||
> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) in 13.2.
|
||||
|
||||
Going a step further, GitLab can show the Code Quality report right
|
||||
in the merge request widget area if a report from the target branch is available to compare to:
|
||||
|
||||
|
|
@ -82,7 +87,7 @@ include:
|
|||
|
||||
The above example will create a `code_quality` job in your CI/CD pipeline which
|
||||
will scan your source code for code quality issues. The report will be saved as a
|
||||
[Code Quality report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality-starter)
|
||||
[Code Quality report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality)
|
||||
that you can later download and analyze.
|
||||
|
||||
It's also possible to override the URL to the Code Quality image by
|
||||
|
|
@ -240,7 +245,7 @@ do this:
|
|||
|
||||
1. Define a job in your `.gitlab-ci.yml` file that generates the
|
||||
[Code Quality report
|
||||
artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality-starter).
|
||||
artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality).
|
||||
1. Configure your tool to generate the Code Quality report artifact as a JSON
|
||||
file that implements a subset of the [Code Climate
|
||||
spec](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types).
|
||||
|
|
@ -276,11 +281,11 @@ NOTE: **Note:**
|
|||
Although the Code Climate spec supports more properties, those are ignored by
|
||||
GitLab.
|
||||
|
||||
## Code Quality reports
|
||||
## Code Quality reports **(STARTER)**
|
||||
|
||||
Once the Code Quality job has completed:
|
||||
|
||||
- The full list of code quality violations generated by a pipeline is available in the
|
||||
- The full list of code quality violations generated by a pipeline is shown in the
|
||||
Code Quality tab of the Pipeline Details page.
|
||||
- Potential changes to code quality are shown directly in the merge request.
|
||||
The Code Quality widget in the merge request compares the reports from the base and head of the branch,
|
||||
|
|
@ -293,7 +298,7 @@ Once the Code Quality job has completed:
|
|||
|
||||
### Using Analysis Plugins
|
||||
|
||||
Should there be a need to extend the default functionality provided by Code Quality, as stated in [Code Quality](#code-quality-starter), [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) are available.
|
||||
Should there be a need to extend the default functionality provided by Code Quality, as stated in [Code Quality](#code-quality), [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) are available.
|
||||
|
||||
For example, to use the [SonarJava analyzer](https://docs.codeclimate.com/docs/sonar-java),
|
||||
add a file named `.codeclimate.yml` containing the [enablement code](https://docs.codeclimate.com/docs/sonar-java#enable-the-plugin)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ A. Consider you're a software developer working in a team:
|
|||
|
||||
1. You checkout a new branch, and submit your changes through a merge request
|
||||
1. You gather feedback from your team
|
||||
1. You work on the implementation optimizing code with [Code Quality reports](code_quality.md) **(STARTER)**
|
||||
1. You work on the implementation optimizing code with [Code Quality reports](code_quality.md)
|
||||
1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD
|
||||
1. You avoid using dependencies whose license is not compatible with your project with [License Compliance reports](../../compliance/license_compliance/index.md) **(ULTIMATE)**
|
||||
1. You request the [approval](merge_request_approvals.md) from your manager **(STARTER)**
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ or link to useful information directly from merge requests:
|
|||
| [Accessibility Testing](accessibility_testing.md) | Automatically report A11y violations for changed pages in merge requests. |
|
||||
| [Browser Performance Testing](browser_performance_testing.md) **(PREMIUM)** | Quickly determine the browser performance impact of pending code changes. |
|
||||
| [Load Performance Testing](load_performance_testing.md) **(PREMIUM)** | Quickly determine the server performance impact of pending code changes. |
|
||||
| [Code Quality](code_quality.md) **(STARTER)** | Analyze your source code quality using the [Code Climate](https://codeclimate.com/) analyzer and show the Code Climate report right in the merge request widget area. |
|
||||
| [Code Quality](code_quality.md) | Analyze your source code quality using the [Code Climate](https://codeclimate.com/) analyzer and show the Code Climate report right in the merge request widget area. |
|
||||
| [Display arbitrary job artifacts](../../../ci/yaml/README.md#artifactsexpose_as) | Configure CI pipelines with the `artifacts:expose_as` parameter to directly link to selected [artifacts](../../../ci/pipelines/job_artifacts.md) in merge requests. |
|
||||
| [GitLab CI/CD](../../../ci/README.md) | Build, test, and deploy your code in a per-branch basis with built-in CI/CD. |
|
||||
| [JUnit test reports](../../../ci/junit_test_reports.md) | Configure your CI jobs to use JUnit test reports, and let GitLab display a report on the merge request so that it’s easier and faster to identify the failure without having to check the entire job log. |
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def map_user_id(jira_user)
|
||||
Gitlab::JiraImport::UserMapper.new(project, jira_user).execute&.id
|
||||
return unless jira_user&.dig('accountId')
|
||||
|
||||
Gitlab::JiraImport.get_user_mapping(project.id, jira_user['accountId'])
|
||||
end
|
||||
|
||||
def reporter
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module JiraImport
|
||||
class UserMapper
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(project, jira_user)
|
||||
@project = project
|
||||
@jira_user = jira_user
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless jira_user
|
||||
|
||||
email = jira_user['emailAddress']
|
||||
|
||||
# We also include emails that are not yet confirmed
|
||||
users = User.by_any_email(email).to_a
|
||||
|
||||
user = users.first
|
||||
|
||||
# this event should never happen but we should log it in case we have invalid data
|
||||
log_user_mapping_message('Multiple users found for an email address', email) if users.count > 1
|
||||
|
||||
unless project.project_member(user) || project.group&.group_member(user)
|
||||
log_user_mapping_message('Jira user not found', email)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :jira_user, :params
|
||||
|
||||
def log_user_mapping_message(message, email)
|
||||
logger.info(
|
||||
project_id: project.id,
|
||||
project_path: project.full_path,
|
||||
user_email: email,
|
||||
message: message
|
||||
)
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Gitlab::Import::Logger.build
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,6 +5,10 @@ module Gitlab
|
|||
def self.install!
|
||||
Sidekiq::Manager.prepend SidekiqVersioning::Manager
|
||||
|
||||
Sidekiq.server_middleware do |chain|
|
||||
chain.add SidekiqVersioning::Middleware
|
||||
end
|
||||
|
||||
# The Sidekiq client API always adds the queue to the Sidekiq queue
|
||||
# list, but mail_room and gitlab-shell do not. This is only necessary
|
||||
# for monitoring.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqVersioning
|
||||
class Middleware
|
||||
def call(worker, job, queue)
|
||||
worker.job_version = job['version']
|
||||
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module SidekiqVersioning
|
||||
module Worker
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
version 0
|
||||
|
||||
attr_writer :job_version
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def version(new_version = nil)
|
||||
if new_version
|
||||
sidekiq_options version: new_version.to_i
|
||||
else
|
||||
get_sidekiq_options['version']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Version is not set if `new.perform` is called directly,
|
||||
# and in that case we fallback to latest version
|
||||
def job_version
|
||||
@job_version ||= self.class.version
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -12748,6 +12748,15 @@ msgstr ""
|
|||
msgid "Integrations|Standard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|This integration has multiple settings available."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Use custom settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Use instance level settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) will be created."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Verify' do
|
||||
RSpec.describe 'Verify', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/229724', type: :investigating } do
|
||||
describe 'Add or Remove CI variable via UI', :smoke do
|
||||
let!(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
|
|
|
|||
|
|
@ -179,6 +179,23 @@ RSpec.describe Projects::ServicesController do
|
|||
|
||||
it_behaves_like 'service update'
|
||||
end
|
||||
|
||||
context 'wehn param `inherit_from_id` is set to empty string' do
|
||||
let(:service_params) { { inherit_from_id: '' } }
|
||||
|
||||
it 'sets inherit_from_id to nil' do
|
||||
expect(service.reload.inherit_from_id).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'wehn param `inherit_from_id` is set to some value' do
|
||||
let(:instance_service) { create(:jira_service, :instance) }
|
||||
let(:service_params) { { inherit_from_id: instance_service.id } }
|
||||
|
||||
it 'sets inherit_from_id to value' do
|
||||
expect(service.reload.inherit_from_id).to eq(instance_service.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'as JSON' do
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe 'Disable individual triggers', :js do
|
||||
include_context 'project service activation'
|
||||
|
||||
let(:checkbox_selector) { 'input[type=checkbox][name$="_events]"]' }
|
||||
let(:checkbox_selector) { 'input[name$="_events]"]' }
|
||||
|
||||
before do
|
||||
visit_project_integration(service_name)
|
||||
|
|
@ -18,7 +18,7 @@ RSpec.describe 'Disable individual triggers', :js do
|
|||
event_count = HipchatService.supported_events.count
|
||||
|
||||
expect(page).to have_content "Trigger"
|
||||
expect(page).to have_css(checkbox_selector, count: event_count)
|
||||
expect(page).to have_css(checkbox_selector, visible: :all, count: event_count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ RSpec.describe 'Disable individual triggers', :js do
|
|||
|
||||
it "doesn't show unnecessary Trigger checkboxes" do
|
||||
expect(page).not_to have_content "Trigger"
|
||||
expect(page).not_to have_css(checkbox_selector)
|
||||
expect(page).not_to have_css(checkbox_selector, visible: :all)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
SIMPLE_BLOB_VIEWER,
|
||||
SIMPLE_BLOB_VIEWER_TITLE,
|
||||
} from '~/blob/components/constants';
|
||||
import { GlButtonGroup, GlDeprecatedButton } from '@gitlab/ui';
|
||||
import { GlButtonGroup, GlButton } from '@gitlab/ui';
|
||||
|
||||
describe('Blob Header Viewer Switcher', () => {
|
||||
let wrapper;
|
||||
|
|
@ -35,7 +35,7 @@ describe('Blob Header Viewer Switcher', () => {
|
|||
beforeEach(() => {
|
||||
createComponent();
|
||||
btnGroup = wrapper.find(GlButtonGroup);
|
||||
buttons = wrapper.findAll(GlDeprecatedButton);
|
||||
buttons = wrapper.findAll(GlButton);
|
||||
});
|
||||
|
||||
it('renders gl-button-group component', () => {
|
||||
|
|
@ -57,7 +57,7 @@ describe('Blob Header Viewer Switcher', () => {
|
|||
|
||||
function factory(propsData = {}) {
|
||||
createComponent(propsData);
|
||||
buttons = wrapper.findAll(GlDeprecatedButton);
|
||||
buttons = wrapper.findAll(GlButton);
|
||||
simpleBtn = buttons.at(0);
|
||||
richBtn = buttons.at(1);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
|
||||
import { GlToggle } from '@gitlab/ui';
|
||||
|
||||
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
|
||||
|
||||
const GL_TOGGLE_ACTIVE_CLASS = 'is-checked';
|
||||
const GL_TOGGLE_DISABLED_CLASS = 'is-disabled';
|
||||
|
||||
describe('ActiveToggle', () => {
|
||||
let wrapper;
|
||||
|
|
@ -11,9 +13,12 @@ describe('ActiveToggle', () => {
|
|||
initialActivated: true,
|
||||
};
|
||||
|
||||
const createComponent = props => {
|
||||
const createComponent = (props = {}, isInheriting = false) => {
|
||||
wrapper = mount(ActiveToggle, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
computed: {
|
||||
isInheriting: () => isInheriting,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -29,6 +34,15 @@ describe('ActiveToggle', () => {
|
|||
const findInputInToggle = () => findGlToggle().find('input');
|
||||
|
||||
describe('template', () => {
|
||||
describe('is inheriting adminSettings', () => {
|
||||
it('renders GlToggle as disabled', () => {
|
||||
createComponent({}, true);
|
||||
|
||||
expect(findGlToggle().exists()).toBe(true);
|
||||
expect(findButtonInToggle().classes()).toContain(GL_TOGGLE_DISABLED_CLASS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialActivated is false', () => {
|
||||
it('renders GlToggle as inactive', () => {
|
||||
createComponent({
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ describe('DynamicField', () => {
|
|||
value: '1',
|
||||
};
|
||||
|
||||
const createComponent = props => {
|
||||
const createComponent = (props, isInheriting = false) => {
|
||||
wrapper = mount(DynamicField, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
computed: {
|
||||
isInheriting: () => isInheriting,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -34,108 +37,143 @@ describe('DynamicField', () => {
|
|||
const findGlFormTextarea = () => wrapper.find(GlFormTextarea);
|
||||
|
||||
describe('template', () => {
|
||||
describe('dynamic field', () => {
|
||||
describe('type is checkbox', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
type: 'checkbox',
|
||||
describe.each([[true, 'disabled', 'readonly'], [false, undefined, undefined]])(
|
||||
'dynamic field, when isInheriting = `%p`',
|
||||
(isInheriting, disabled, readonly) => {
|
||||
describe('type is checkbox', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(
|
||||
{
|
||||
type: 'checkbox',
|
||||
},
|
||||
isInheriting,
|
||||
);
|
||||
});
|
||||
|
||||
it(`renders GlFormCheckbox, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(true);
|
||||
expect(
|
||||
findGlFormCheckbox()
|
||||
.find('[type=checkbox]')
|
||||
.attributes('disabled'),
|
||||
).toBe(disabled);
|
||||
});
|
||||
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
expect(findGlFormInput().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders GlFormCheckbox', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(true);
|
||||
});
|
||||
describe('type is select', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(
|
||||
{
|
||||
type: 'select',
|
||||
choices: [['all', 'All details'], ['standard', 'Standard']],
|
||||
},
|
||||
isInheriting,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
expect(findGlFormInput().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
it(`renders GlFormSelect, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
|
||||
expect(findGlFormSelect().exists()).toBe(true);
|
||||
expect(findGlFormSelect().findAll('option')).toHaveLength(2);
|
||||
expect(
|
||||
findGlFormSelect()
|
||||
.find('select')
|
||||
.attributes('disabled'),
|
||||
).toBe(disabled);
|
||||
});
|
||||
|
||||
describe('type is select', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
type: 'select',
|
||||
choices: [['all', 'All details'], ['standard', 'Standard']],
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
expect(findGlFormInput().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders findGlFormSelect', () => {
|
||||
expect(findGlFormSelect().exists()).toBe(true);
|
||||
expect(findGlFormSelect().findAll('option')).toHaveLength(2);
|
||||
});
|
||||
describe('type is textarea', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(
|
||||
{
|
||||
type: 'textarea',
|
||||
},
|
||||
isInheriting,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
expect(findGlFormInput().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
|
||||
expect(findGlFormTextarea().exists()).toBe(true);
|
||||
expect(
|
||||
findGlFormTextarea()
|
||||
.find('textarea')
|
||||
.attributes('readonly'),
|
||||
).toBe(readonly);
|
||||
});
|
||||
|
||||
describe('type is textarea', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
type: 'textarea',
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormInput().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders findGlFormTextarea', () => {
|
||||
expect(findGlFormTextarea().exists()).toBe(true);
|
||||
});
|
||||
describe('type is password', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(
|
||||
{
|
||||
type: 'password',
|
||||
},
|
||||
isInheriting,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormInput().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
|
||||
expect(findGlFormInput().exists()).toBe(true);
|
||||
expect(findGlFormInput().attributes('type')).toBe('password');
|
||||
expect(findGlFormInput().attributes('readonly')).toBe(readonly);
|
||||
});
|
||||
|
||||
describe('type is password', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
type: 'password',
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders GlFormInput', () => {
|
||||
expect(findGlFormInput().exists()).toBe(true);
|
||||
expect(findGlFormInput().attributes('type')).toBe('password');
|
||||
});
|
||||
describe('type is text', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(
|
||||
{
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
isInheriting,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
|
||||
expect(findGlFormInput().exists()).toBe(true);
|
||||
expect(findGlFormInput().attributes()).toMatchObject({
|
||||
type: 'text',
|
||||
id: 'service_project_url',
|
||||
name: 'service[project_url]',
|
||||
placeholder: defaultProps.placeholder,
|
||||
required: 'required',
|
||||
});
|
||||
expect(findGlFormInput().attributes('readonly')).toBe(readonly);
|
||||
});
|
||||
|
||||
describe('type is text', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
type: 'text',
|
||||
required: true,
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders GlFormInput', () => {
|
||||
expect(findGlFormInput().exists()).toBe(true);
|
||||
expect(findGlFormInput().attributes()).toMatchObject({
|
||||
type: 'text',
|
||||
id: 'service_project_url',
|
||||
name: 'service[project_url]',
|
||||
placeholder: defaultProps.placeholder,
|
||||
required: 'required',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render other types of input', () => {
|
||||
expect(findGlFormCheckbox().exists()).toBe(false);
|
||||
expect(findGlFormSelect().exists()).toBe(false);
|
||||
expect(findGlFormTextarea().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('help text', () => {
|
||||
it('renders description with help text', () => {
|
||||
|
|
|
|||
|
|
@ -1,34 +1,29 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from '~/integrations/edit/store';
|
||||
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
|
||||
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
|
||||
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
|
||||
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
|
||||
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
|
||||
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
|
||||
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
|
||||
import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
|
||||
|
||||
describe('IntegrationForm', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
activeToggleProps: {
|
||||
initialActivated: true,
|
||||
},
|
||||
showActive: true,
|
||||
triggerFieldsProps: {
|
||||
initialTriggerCommit: false,
|
||||
initialTriggerMergeRequest: false,
|
||||
initialEnableComments: false,
|
||||
},
|
||||
jiraIssuesProps: {},
|
||||
type: '',
|
||||
};
|
||||
|
||||
const createComponent = (props, featureFlags = {}) => {
|
||||
const createComponent = (customStateProps = {}, featureFlags = {}, initialState = {}) => {
|
||||
wrapper = shallowMount(IntegrationForm, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
propsData: {},
|
||||
store: createStore({
|
||||
customState: { ...mockIntegrationProps, ...customStateProps },
|
||||
...initialState,
|
||||
}),
|
||||
stubs: {
|
||||
OverrideDropdown,
|
||||
ActiveToggle,
|
||||
JiraTriggerFields,
|
||||
TriggerFields,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: featureFlags,
|
||||
|
|
@ -43,6 +38,7 @@ describe('IntegrationForm', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
|
||||
const findActiveToggle = () => wrapper.find(ActiveToggle);
|
||||
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
|
||||
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
|
||||
|
|
@ -140,5 +136,35 @@ describe('IntegrationForm', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('adminState state is null', () => {
|
||||
it('does not render OverrideDropdown', () => {
|
||||
createComponent(
|
||||
{},
|
||||
{},
|
||||
{
|
||||
adminState: null,
|
||||
},
|
||||
);
|
||||
|
||||
expect(findOverrideDropdown().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adminState state is an object', () => {
|
||||
it('renders OverrideDropdown', () => {
|
||||
createComponent(
|
||||
{},
|
||||
{},
|
||||
{
|
||||
adminState: {
|
||||
...mockIntegrationProps,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(findOverrideDropdown().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
|
||||
|
||||
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
|
||||
|
||||
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
|
||||
|
||||
describe('JiraIssuesFields', () => {
|
||||
let wrapper;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
|
||||
import { GlFormCheckbox } from '@gitlab/ui';
|
||||
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
|
||||
|
||||
describe('JiraTriggerFields', () => {
|
||||
let wrapper;
|
||||
|
|
@ -11,9 +11,12 @@ describe('JiraTriggerFields', () => {
|
|||
initialEnableComments: false,
|
||||
};
|
||||
|
||||
const createComponent = props => {
|
||||
const createComponent = (props, isInheriting = false) => {
|
||||
wrapper = mount(JiraTriggerFields, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
computed: {
|
||||
isInheriting: () => isInheriting,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -93,5 +96,23 @@ describe('JiraTriggerFields', () => {
|
|||
expect(findCommentDetail().isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('disables checkboxes and radios if inheriting', () => {
|
||||
createComponent(
|
||||
{
|
||||
initialTriggerCommit: true,
|
||||
initialEnableComments: true,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
wrapper.findAll('[type=checkbox]').wrappers.forEach(checkbox => {
|
||||
expect(checkbox.attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
wrapper.findAll('[type=radio]').wrappers.forEach(radio => {
|
||||
expect(radio.attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ describe('TriggerFields', () => {
|
|||
type: 'slack',
|
||||
};
|
||||
|
||||
const createComponent = props => {
|
||||
const createComponent = (props, isInheriting = false) => {
|
||||
wrapper = mount(TriggerFields, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
computed: {
|
||||
isInheriting: () => isInheriting,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -22,10 +25,11 @@ describe('TriggerFields', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAll(GlFormGroup);
|
||||
const findAllGlFormCheckboxes = () => wrapper.findAll(GlFormCheckbox);
|
||||
const findAllGlFormInputs = () => wrapper.findAll(GlFormInput);
|
||||
|
||||
describe('template', () => {
|
||||
describe.each([true, false])('template, isInheriting = `%p`', isInheriting => {
|
||||
it('renders a label with text "Trigger"', () => {
|
||||
createComponent();
|
||||
|
||||
|
|
@ -51,9 +55,12 @@ describe('TriggerFields', () => {
|
|||
];
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
events,
|
||||
});
|
||||
createComponent(
|
||||
{
|
||||
events,
|
||||
},
|
||||
isInheriting,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render GlFormInput for each event', () => {
|
||||
|
|
@ -69,8 +76,10 @@ describe('TriggerFields', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders GlFormCheckbox for each event', () => {
|
||||
const checkboxes = findAllGlFormCheckboxes();
|
||||
it(`renders GlFormCheckbox and corresponding hidden input for each event, which ${
|
||||
isInheriting ? 'is' : 'is not'
|
||||
} disabled`, () => {
|
||||
const checkboxes = findAllGlFormGroups();
|
||||
const expectedResults = [
|
||||
{ labelText: 'Push', inputName: 'service[push_event]' },
|
||||
{ labelText: 'Merge Request', inputName: 'service[merge_requests_event]' },
|
||||
|
|
@ -78,14 +87,22 @@ describe('TriggerFields', () => {
|
|||
expect(checkboxes).toHaveLength(2);
|
||||
|
||||
checkboxes.wrappers.forEach((checkbox, index) => {
|
||||
const checkBox = checkbox.find(GlFormCheckbox);
|
||||
|
||||
expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
|
||||
expect(checkbox.find('input').attributes('name')).toBe(expectedResults[index].inputName);
|
||||
expect(checkbox.vm.$attrs.checked).toBe(events[index].value);
|
||||
expect(checkbox.find('[type=hidden]').attributes('name')).toBe(
|
||||
expectedResults[index].inputName,
|
||||
);
|
||||
expect(checkbox.find('[type=hidden]').attributes('value')).toBe(
|
||||
events[index].value.toString(),
|
||||
);
|
||||
expect(checkBox.vm.$attrs.disabled).toBe(isInheriting);
|
||||
expect(checkBox.vm.$attrs.checked).toBe(events[index].value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('events with field property', () => {
|
||||
describe('events with field property, isInheriting = `%p`', () => {
|
||||
const events = [
|
||||
{
|
||||
field: {
|
||||
|
|
@ -102,16 +119,21 @@ describe('TriggerFields', () => {
|
|||
];
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
events,
|
||||
});
|
||||
createComponent(
|
||||
{
|
||||
events,
|
||||
},
|
||||
isInheriting,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders GlFormCheckbox for each event', () => {
|
||||
expect(findAllGlFormCheckboxes()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders GlFormInput for each event', () => {
|
||||
it(`renders GlFormInput for each event, which ${
|
||||
isInheriting ? 'is' : 'is not'
|
||||
} readonly`, () => {
|
||||
const fields = findAllGlFormInputs();
|
||||
const expectedResults = [
|
||||
{
|
||||
|
|
@ -128,6 +150,7 @@ describe('TriggerFields', () => {
|
|||
|
||||
fields.wrappers.forEach((field, index) => {
|
||||
expect(field.attributes()).toMatchObject(expectedResults[index]);
|
||||
expect(field.vm.$attrs.readonly).toBe(isInheriting);
|
||||
expect(field.vm.$attrs.value).toBe(events[index].field.value);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const mockIntegrationProps = {
|
||||
id: 25,
|
||||
activeToggleProps: {
|
||||
initialActivated: true,
|
||||
},
|
||||
showActive: true,
|
||||
triggerFieldsProps: {
|
||||
initialTriggerCommit: false,
|
||||
initialTriggerMergeRequest: false,
|
||||
initialEnableComments: false,
|
||||
},
|
||||
jiraIssuesProps: {},
|
||||
triggerEvents: [],
|
||||
fields: [],
|
||||
type: '',
|
||||
inheritFromId: 25,
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import createState from '~/integrations/edit/store/state';
|
||||
import { setOverride } from '~/integrations/edit/store/actions';
|
||||
import * as types from '~/integrations/edit/store/mutation_types';
|
||||
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
|
||||
describe('Integration form store actions', () => {
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
});
|
||||
|
||||
describe('setOverride', () => {
|
||||
it('should commit override mutation', () => {
|
||||
return testAction(setOverride, true, state, [{ type: types.SET_OVERRIDE, payload: true }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { currentKey, isInheriting, propsSource } from '~/integrations/edit/store/getters';
|
||||
import createState from '~/integrations/edit/store/state';
|
||||
import { mockIntegrationProps } from '../mock_data';
|
||||
|
||||
describe('Integration form store getters', () => {
|
||||
let state;
|
||||
const customState = { ...mockIntegrationProps, type: 'CustomState' };
|
||||
const adminState = { ...mockIntegrationProps, type: 'AdminState' };
|
||||
|
||||
beforeEach(() => {
|
||||
state = createState({ customState });
|
||||
});
|
||||
|
||||
describe('isInheriting', () => {
|
||||
describe('when adminState is null', () => {
|
||||
it('returns false', () => {
|
||||
expect(isInheriting(state)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adminState is an object', () => {
|
||||
beforeEach(() => {
|
||||
state.adminState = adminState;
|
||||
});
|
||||
|
||||
describe('when override is false', () => {
|
||||
beforeEach(() => {
|
||||
state.override = false;
|
||||
});
|
||||
|
||||
it('returns false', () => {
|
||||
expect(isInheriting(state)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when override is true', () => {
|
||||
beforeEach(() => {
|
||||
state.override = true;
|
||||
});
|
||||
|
||||
it('returns true', () => {
|
||||
expect(isInheriting(state)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('propsSource', () => {
|
||||
beforeEach(() => {
|
||||
state.adminState = adminState;
|
||||
});
|
||||
|
||||
it('equals adminState if inheriting', () => {
|
||||
expect(propsSource(state, { isInheriting: true })).toEqual(adminState);
|
||||
});
|
||||
|
||||
it('equals customState if not inheriting', () => {
|
||||
expect(propsSource(state, { isInheriting: false })).toEqual(customState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentKey', () => {
|
||||
it('equals `admin` if inheriting', () => {
|
||||
expect(currentKey(state, { isInheriting: true })).toEqual('admin');
|
||||
});
|
||||
|
||||
it('equals `custom` if not inheriting', () => {
|
||||
expect(currentKey(state, { isInheriting: false })).toEqual('custom');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import mutations from '~/integrations/edit/store/mutations';
|
||||
import createState from '~/integrations/edit/store/state';
|
||||
import * as types from '~/integrations/edit/store/mutation_types';
|
||||
|
||||
describe('Integration form store mutations', () => {
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
});
|
||||
|
||||
describe(`${types.SET_OVERRIDE}`, () => {
|
||||
it('sets override', () => {
|
||||
mutations[types.SET_OVERRIDE](state, true);
|
||||
|
||||
expect(state.override).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import createState from '~/integrations/edit/store/state';
|
||||
|
||||
describe('Integration form state factory', () => {
|
||||
it('states default to null', () => {
|
||||
expect(createState()).toEqual({
|
||||
adminState: null,
|
||||
customState: {},
|
||||
override: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('override is initialized correctly', () => {
|
||||
it.each([
|
||||
[{ id: 25 }, { inheritFromId: null }, true],
|
||||
[{ id: 25 }, { inheritFromId: 27 }, true],
|
||||
[{ id: 25 }, { inheritFromId: 25 }, false],
|
||||
[null, { inheritFromId: null }, false],
|
||||
[null, { inheritFromId: 25 }, false],
|
||||
])(
|
||||
'for adminState: %p, customState: %p: override = `%p`',
|
||||
(adminState, customState, expected) => {
|
||||
expect(createState({ adminState, customState }).override).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { insertMarkdownText, keypressNoteText } from '~/lib/utils/text_markdown';
|
||||
import { insertMarkdownText } from '~/lib/utils/text_markdown';
|
||||
|
||||
describe('init markdown', () => {
|
||||
let textArea;
|
||||
|
|
@ -115,15 +115,14 @@ describe('init markdown', () => {
|
|||
describe('with selection', () => {
|
||||
const text = 'initial selected value';
|
||||
const selected = 'selected';
|
||||
let selectedIndex;
|
||||
|
||||
beforeEach(() => {
|
||||
textArea.value = text;
|
||||
selectedIndex = text.indexOf(selected);
|
||||
const selectedIndex = text.indexOf(selected);
|
||||
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
|
||||
});
|
||||
|
||||
it('applies the tag to the selected value', () => {
|
||||
const selectedIndex = text.indexOf(selected);
|
||||
const tag = '*';
|
||||
|
||||
insertMarkdownText({
|
||||
|
|
@ -154,29 +153,6 @@ describe('init markdown', () => {
|
|||
expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`));
|
||||
});
|
||||
|
||||
it.each`
|
||||
key | expected
|
||||
${'['} | ${`[${selected}]`}
|
||||
${'*'} | ${`**${selected}**`}
|
||||
${"'"} | ${`'${selected}'`}
|
||||
${'_'} | ${`_${selected}_`}
|
||||
${'`'} | ${`\`${selected}\``}
|
||||
${'"'} | ${`"${selected}"`}
|
||||
${'{'} | ${`{${selected}}`}
|
||||
${'('} | ${`(${selected})`}
|
||||
${'<'} | ${`<${selected}>`}
|
||||
`('generates $expected when $key is pressed', ({ key, expected }) => {
|
||||
const event = new KeyboardEvent('keydown', { key });
|
||||
|
||||
textArea.addEventListener('keydown', keypressNoteText);
|
||||
textArea.dispatchEvent(event);
|
||||
|
||||
expect(textArea.value).toEqual(text.replace(selected, expected));
|
||||
|
||||
// cursor placement should be after selection + 2 tag lengths
|
||||
expect(textArea.selectionStart).toBe(selectedIndex + expected.length);
|
||||
});
|
||||
|
||||
describe('and text to be selected', () => {
|
||||
const tag = '[{text}](url)';
|
||||
const select = 'url';
|
||||
|
|
@ -202,7 +178,7 @@ describe('init markdown', () => {
|
|||
it('selects the right text when multiple tags are present', () => {
|
||||
const initialValue = `${tag} ${tag} ${selected}`;
|
||||
textArea.value = initialValue;
|
||||
selectedIndex = initialValue.indexOf(selected);
|
||||
const selectedIndex = initialValue.indexOf(selected);
|
||||
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
|
||||
insertMarkdownText({
|
||||
textArea,
|
||||
|
|
@ -228,7 +204,7 @@ describe('init markdown', () => {
|
|||
const initialValue = `text ${expectedUrl} text`;
|
||||
|
||||
textArea.value = initialValue;
|
||||
selectedIndex = initialValue.indexOf(expectedUrl);
|
||||
const selectedIndex = initialValue.indexOf(expectedUrl);
|
||||
textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length);
|
||||
|
||||
insertMarkdownText({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import component from '~/reports/codequality_report/components/codequality_issue_body.vue';
|
||||
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
|
||||
|
||||
describe('code quality issue body issue body', () => {
|
||||
let wrapper;
|
||||
|
||||
const codequalityIssue = {
|
||||
name:
|
||||
'rubygem-rest-client: session fixation vulnerability via Set-Cookie headers in 30x redirection responses',
|
||||
path: 'Gemfile.lock',
|
||||
severity: 'normal',
|
||||
type: 'Issue',
|
||||
urlPath: '/Gemfile.lock#L22',
|
||||
};
|
||||
|
||||
const mountWithStatus = initialStatus => {
|
||||
wrapper = shallowMount(component, {
|
||||
propsData: {
|
||||
issue: codequalityIssue,
|
||||
status: initialStatus,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('with success', () => {
|
||||
it('renders fixed label', () => {
|
||||
mountWithStatus(STATUS_SUCCESS);
|
||||
|
||||
expect(wrapper.text()).toContain('Fixed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without success', () => {
|
||||
it('renders fixed label', () => {
|
||||
mountWithStatus(STATUS_FAILED);
|
||||
|
||||
expect(wrapper.text()).not.toContain('Fixed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('name', () => {
|
||||
it('renders name', () => {
|
||||
mountWithStatus(STATUS_NEUTRAL);
|
||||
|
||||
expect(wrapper.text()).toContain(codequalityIssue.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('path', () => {
|
||||
it('renders the report-link path using the correct code quality issue', () => {
|
||||
mountWithStatus(STATUS_NEUTRAL);
|
||||
|
||||
expect(wrapper.find('report-link-stub').props('issue')).toBe(codequalityIssue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
|
||||
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
|
||||
import store from '~/reports/codequality_report/store';
|
||||
import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('Grouped code quality reports app', () => {
|
||||
const Component = localVue.extend(GroupedCodequalityReportsApp);
|
||||
let wrapper;
|
||||
let mockStore;
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
wrapper = mount(Component, {
|
||||
store: mockStore,
|
||||
localVue,
|
||||
propsData: {
|
||||
basePath: 'base.json',
|
||||
headPath: 'head.json',
|
||||
baseBlobPath: 'base/blob/path/',
|
||||
headBlobPath: 'head/blob/path/',
|
||||
codequalityHelpPath: 'codequality_help.html',
|
||||
...props,
|
||||
},
|
||||
methods: {
|
||||
fetchReports: () => {},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findWidget = () => wrapper.find('.js-codequality-widget');
|
||||
const findIssueBody = () => wrapper.find(CodequalityIssueBody);
|
||||
|
||||
beforeEach(() => {
|
||||
mockStore = store();
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when it is loading reports', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.isLoading = true;
|
||||
});
|
||||
|
||||
it('should render loading text', () => {
|
||||
expect(findWidget().text()).toEqual('Loading codeclimate report');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when base and head reports are loaded and compared', () => {
|
||||
describe('with no issues', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.newIssues = [];
|
||||
mockStore.state.resolvedIssues = [];
|
||||
});
|
||||
|
||||
it('renders no changes text', () => {
|
||||
expect(findWidget().text()).toEqual('No changes to code quality');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with issues', () => {
|
||||
describe('with new issues', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
|
||||
mockStore.state.resolvedIssues = [];
|
||||
});
|
||||
|
||||
it('renders summary text', () => {
|
||||
expect(findWidget().text()).toContain('Code quality degraded on 1 point');
|
||||
});
|
||||
|
||||
it('renders custom codequality issue body', () => {
|
||||
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with resolved issues', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.newIssues = [];
|
||||
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
|
||||
});
|
||||
|
||||
it('renders summary text', () => {
|
||||
expect(findWidget().text()).toContain('Code quality improved on 1 point');
|
||||
});
|
||||
|
||||
it('renders custom codequality issue body', () => {
|
||||
expect(findIssueBody().props('issue')).toEqual(mockParsedBaseIssues[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with new and resolved issues', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
|
||||
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
|
||||
});
|
||||
|
||||
it('renders summary text', () => {
|
||||
expect(findWidget().text()).toContain(
|
||||
'Code quality improved on 1 point and degraded on 1 point',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders custom codequality issue body', () => {
|
||||
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is a head report but no base report', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.basePath = null;
|
||||
mockStore.state.hasError = true;
|
||||
});
|
||||
|
||||
it('renders error text', () => {
|
||||
expect(findWidget().text()).toEqual('Failed to load codeclimate report');
|
||||
});
|
||||
|
||||
it('renders a help icon with more information', () => {
|
||||
expect(findWidget().html()).toContain('ic-question');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on error', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.hasError = true;
|
||||
});
|
||||
|
||||
it('renders error text', () => {
|
||||
expect(findWidget().text()).toContain('Failed to load codeclimate report');
|
||||
});
|
||||
|
||||
it('does not render a help icon', () => {
|
||||
expect(findWidget().html()).not.toContain('ic-question');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -211,6 +211,15 @@ export default {
|
|||
can_revert_on_current_merge_request: true,
|
||||
can_cherry_pick_on_current_merge_request: true,
|
||||
},
|
||||
codeclimate: {
|
||||
head_path: 'head.json',
|
||||
base_path: 'base.json',
|
||||
},
|
||||
blob_path: {
|
||||
base_path: 'blob_path',
|
||||
head_path: 'blob_path',
|
||||
},
|
||||
codequality_help_path: 'code_quality.html',
|
||||
target_branch_path: '/root/acets-app/branches/master',
|
||||
source_branch_path: '/root/acets-app/branches/daaaa',
|
||||
conflict_resolution_ui_path: '/root/acets-app/-/merge_requests/22/conflicts',
|
||||
|
|
|
|||
|
|
@ -609,6 +609,12 @@ describe('mrWidgetOptions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('code quality widget', () => {
|
||||
it('renders the component', () => {
|
||||
expect(vm.$el.querySelector('.js-codequality-widget')).toExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipeline for target branch after merge', () => {
|
||||
describe('with information for target branch pipeline', () => {
|
||||
beforeEach(done => {
|
||||
|
|
|
|||
|
|
@ -49,14 +49,18 @@ describe('getStateKey', () => {
|
|||
|
||||
expect(bound()).toEqual('unresolvedDiscussions');
|
||||
|
||||
data.work_in_progress = true;
|
||||
|
||||
expect(bound()).toEqual('workInProgress');
|
||||
|
||||
context.onlyAllowMergeIfPipelineSucceeds = true;
|
||||
context.isPipelineFailed = true;
|
||||
|
||||
expect(bound()).toEqual('pipelineFailed');
|
||||
|
||||
data.work_in_progress = true;
|
||||
context.shouldBeRebased = true;
|
||||
|
||||
expect(bound()).toEqual('workInProgress');
|
||||
expect(bound()).toEqual('rebase');
|
||||
|
||||
data.has_conflicts = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,28 @@ RSpec.describe ServicesHelper do
|
|||
it { expect(event_action_title('comment')).to eq 'Comment' }
|
||||
it { expect(event_action_title('something')).to eq 'Something' }
|
||||
end
|
||||
|
||||
describe '#integration_form_data' do
|
||||
subject { helper.integration_form_data(integration) }
|
||||
|
||||
context 'Jira service' do
|
||||
let(:integration) { build(:jira_service) }
|
||||
|
||||
it 'includes Jira specific fields' do
|
||||
is_expected.to include(
|
||||
:id,
|
||||
:show_active,
|
||||
:activated,
|
||||
:type,
|
||||
:merge_request_events,
|
||||
:commit_events,
|
||||
:enable_comments,
|
||||
:comment_detail,
|
||||
:trigger_events,
|
||||
:fields,
|
||||
:inherit_from_id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
|
|||
let_it_be(:other_project_label) { create(:label, project: project, title: 'feature') }
|
||||
let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:iid) { 5 }
|
||||
let(:key) { 'PROJECT-5' }
|
||||
|
|
@ -17,8 +18,8 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
|
|||
let(:description) { 'basic description' }
|
||||
let(:created_at) { '2020-01-01 20:00:00' }
|
||||
let(:updated_at) { '2020-01-10 20:00:00' }
|
||||
let(:assignee) { double(attrs: { 'displayName' => 'Solver', 'emailAddress' => 'assignee@example.com' }) }
|
||||
let(:reporter) { double(attrs: { 'displayName' => 'Reporter', 'emailAddress' => 'reporter@example.com' }) }
|
||||
let(:assignee) { nil }
|
||||
let(:reporter) { nil }
|
||||
let(:jira_status) { 'new' }
|
||||
|
||||
let(:parent_field) do
|
||||
|
|
@ -109,11 +110,12 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
|
|||
end
|
||||
|
||||
context 'author' do
|
||||
context 'when reporter maps to a valid GitLab user' do
|
||||
let!(:user) { create(:user, email: 'reporter@example.com') }
|
||||
let(:reporter) { double(attrs: { 'displayName' => 'Solver', 'accountId' => 'abcd' }) }
|
||||
|
||||
context 'when reporter maps to a valid GitLab user' do
|
||||
it 'sets the issue author to the mapped user' do
|
||||
project.add_developer(user)
|
||||
expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, 'abcd')
|
||||
.and_return(user.id)
|
||||
|
||||
expect(subject[:author_id]).to eq(user.id)
|
||||
end
|
||||
|
|
@ -121,6 +123,9 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
|
|||
|
||||
context 'when reporter does not map to a valid Gitlab user' do
|
||||
it 'defaults the issue author to project creator' do
|
||||
expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, 'abcd')
|
||||
.and_return(nil)
|
||||
|
||||
expect(subject[:author_id]).to eq(current_user.id)
|
||||
end
|
||||
end
|
||||
|
|
@ -129,25 +134,30 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
|
|||
let(:reporter) { nil }
|
||||
|
||||
it 'defaults the issue author to project creator' do
|
||||
expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
|
||||
|
||||
expect(subject[:author_id]).to eq(current_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when reporter field is missing email address' do
|
||||
context 'when reporter field is missing accountId' do
|
||||
let(:reporter) { double(attrs: { 'displayName' => 'Reporter' }) }
|
||||
|
||||
it 'defaults the issue author to project creator' do
|
||||
expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
|
||||
|
||||
expect(subject[:author_id]).to eq(current_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'assignee' do
|
||||
context 'when assignee maps to a valid GitLab user' do
|
||||
let!(:user) { create(:user, email: 'assignee@example.com') }
|
||||
let(:assignee) { double(attrs: { 'displayName' => 'Solver', 'accountId' => '1234' }) }
|
||||
|
||||
context 'when assignee maps to a valid GitLab user' do
|
||||
it 'sets the issue assignees to the mapped user' do
|
||||
project.add_developer(user)
|
||||
expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, '1234')
|
||||
.and_return(user.id)
|
||||
|
||||
expect(subject[:assignee_ids]).to eq([user.id])
|
||||
end
|
||||
|
|
@ -155,6 +165,9 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
|
|||
|
||||
context 'when assignee does not map to a valid GitLab user' do
|
||||
it 'leaves the assignee empty' do
|
||||
expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, '1234')
|
||||
.and_return(nil)
|
||||
|
||||
expect(subject[:assignee_ids]).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
@ -163,14 +176,18 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
|
|||
let(:assignee) { nil }
|
||||
|
||||
it 'leaves the assignee empty' do
|
||||
expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
|
||||
|
||||
expect(subject[:assignee_ids]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when assginee field is missing email address' do
|
||||
let(:assignee) { double(attrs: { 'displayName' => 'Reporter' }) }
|
||||
context 'when assginee field is missing accountId' do
|
||||
let(:assignee) { double(attrs: { 'displayName' => 'Solver' }) }
|
||||
|
||||
it 'leaves the assignee empty' do
|
||||
expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
|
||||
|
||||
expect(subject[:assignee_ids]).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::JiraImport::UserMapper do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:user) { create(:user, email: 'user@example.com') }
|
||||
let_it_be(:email) { create(:email, user: user, email: 'second_email@example.com', confirmed_at: nil) }
|
||||
|
||||
let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'user@example.com' } }
|
||||
|
||||
describe '#execute' do
|
||||
subject { described_class.new(project, jira_user).execute }
|
||||
|
||||
context 'when jira_user is nil' do
|
||||
let(:jira_user) { nil }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Gitlab user is not found by email' do
|
||||
let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'other@example.com' } }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when jira_user emailAddress is nil' do
|
||||
let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => nil } }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when jira_user emailAddress key is missing' do
|
||||
let(:jira_user) { { 'acountId' => '1a2b' } }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when found user is not a project member' do
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when found user is a project member' do
|
||||
it 'returns the found user' do
|
||||
project.add_developer(user)
|
||||
|
||||
expect(subject).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user found by unconfirmd secondary address is a project member' do
|
||||
let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'second_email@example.com' } }
|
||||
|
||||
it 'returns the found user' do
|
||||
project.add_developer(user)
|
||||
|
||||
expect(subject).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is a group member' do
|
||||
it 'returns the found user' do
|
||||
group.add_developer(user)
|
||||
|
||||
expect(subject).to eq(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::SidekiqVersioning::Middleware do
|
||||
let(:worker_class) do
|
||||
Class.new do
|
||||
def self.name
|
||||
'DummyWorker'
|
||||
end
|
||||
|
||||
include ApplicationWorker
|
||||
|
||||
version 2
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
let(:worker) { worker_class.new }
|
||||
let(:job) { { 'version' => 3, 'queue' => queue } }
|
||||
let(:queue) { worker_class.queue }
|
||||
|
||||
def call!(&block)
|
||||
block ||= -> {}
|
||||
subject.call(worker, job, queue, &block)
|
||||
end
|
||||
|
||||
it 'sets worker.job_version' do
|
||||
call!
|
||||
|
||||
expect(worker.job_version).to eq(job['version'])
|
||||
end
|
||||
|
||||
it 'yields' do
|
||||
expect { |b| call!(&b) }.to yield_control
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::SidekiqVersioning::Worker do
|
||||
let(:worker) do
|
||||
Class.new do
|
||||
def self.name
|
||||
'DummyWorker'
|
||||
end
|
||||
|
||||
# ApplicationWorker includes Gitlab::SidekiqVersioning::Worker
|
||||
include ApplicationWorker
|
||||
|
||||
version 2
|
||||
end
|
||||
end
|
||||
|
||||
describe '.version' do
|
||||
context 'when called with an argument' do
|
||||
it 'sets the version option' do
|
||||
worker.version 3
|
||||
|
||||
expect(worker.get_sidekiq_options['version']).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when called without an argument' do
|
||||
it 'returns the version option' do
|
||||
worker.sidekiq_options version: 3
|
||||
|
||||
expect(worker.version).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#job_version' do
|
||||
let(:job) { worker.new }
|
||||
|
||||
context 'when job_version is not set' do
|
||||
it 'returns latest version' do
|
||||
expect(job.job_version).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job_version is set' do
|
||||
it 'returns the set version' do
|
||||
job.job_version = 0
|
||||
|
||||
expect(job.job_version).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -35,6 +35,12 @@ RSpec.describe Gitlab::SidekiqVersioning, :redis do
|
|||
expect(Sidekiq::Manager).to include(Gitlab::SidekiqVersioning::Manager)
|
||||
end
|
||||
|
||||
it 'adds the SidekiqVersioning::Middleware Sidekiq server middleware' do
|
||||
described_class.install!
|
||||
|
||||
expect(Sidekiq.server_middleware.entries.map(&:klass)).to include(Gitlab::SidekiqVersioning::Middleware)
|
||||
end
|
||||
|
||||
it 'registers all versionless and versioned queues with Redis' do
|
||||
described_class.install!
|
||||
|
||||
|
|
|
|||
|
|
@ -386,6 +386,33 @@ RSpec.describe Service do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'instance' do
|
||||
describe '.instance_for' do
|
||||
let_it_be(:jira_service) { create(:jira_service, :instance) }
|
||||
let_it_be(:slack_service) { create(:slack_service, :instance) }
|
||||
|
||||
subject { described_class.instance_for(type) }
|
||||
|
||||
context 'Hipchat serivce' do
|
||||
let(:type) { 'HipchatService' }
|
||||
|
||||
it { is_expected.to eq(nil) }
|
||||
end
|
||||
|
||||
context 'Jira serivce' do
|
||||
let(:type) { 'JiraService' }
|
||||
|
||||
it { is_expected.to eq(jira_service) }
|
||||
end
|
||||
|
||||
context 'Slack serivce' do
|
||||
let(:type) { 'SlackService' }
|
||||
|
||||
it { is_expected.to eq(slack_service) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "{property}_changed?" do
|
||||
let(:service) do
|
||||
BambooService.create(
|
||||
|
|
|
|||
Loading…
Reference in New Issue