Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-24 18:09:28 +00:00
parent 83516c6aa6
commit ba44d5ef19
13 changed files with 349 additions and 19 deletions

View File

@ -8,6 +8,7 @@ import {
GlButton,
GlCard,
} from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
export default {
@ -20,18 +21,36 @@ export default {
GlLink,
GlButton,
GlCard,
JiraIssueCreationVulnerabilities: () =>
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
mixins: [glFeatureFlagsMixin()],
props: {
showJiraIssuesIntegration: {
type: Boolean,
required: false,
default: false,
},
showJiraVulnerabilitiesIntegration: {
type: Boolean,
required: false,
default: false,
},
initialEnableJiraIssues: {
type: Boolean,
required: false,
default: null,
},
initialEnableJiraVulnerabilities: {
type: Boolean,
required: false,
default: false,
},
initialVulnerabilitiesIssuetype: {
type: String,
required: false,
default: '',
},
initialProjectKey: {
type: String,
required: false,
@ -45,12 +64,12 @@ export default {
upgradePlanPath: {
type: String,
required: false,
default: null,
default: '',
},
editProjectPath: {
type: String,
required: false,
default: null,
default: '',
},
},
data() {
@ -64,6 +83,13 @@ export default {
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
showJiraVulnerabilitiesOptions() {
return (
this.enableJiraIssues &&
this.showJiraVulnerabilitiesIntegration &&
this.glFeatures.jiraForVulnerabilities
);
},
},
created() {
eventHub.$on('validateForm', this.validateForm);
@ -75,6 +101,9 @@ export default {
validateForm() {
this.validated = true;
},
getJiraIssueTypes() {
eventHub.$emit('getJiraIssueTypes');
},
},
};
</script>
@ -105,6 +134,14 @@ export default {
}}
</template>
</gl-form-checkbox>
<jira-issue-creation-vulnerabilities
v-if="showJiraVulnerabilitiesOptions"
:project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
data-testid="jira-for-vulnerabilities"
@request-get-issue-types="getJiraIssueTypes"
/>
</template>
<gl-card v-else class="gl-mt-7">
<strong>{{ __('This is a Premium feature') }}</strong>

View File

@ -27,6 +27,7 @@ function parseDatasetToProps(data) {
cancelPath,
testPath,
resetPath,
vulnerabilitiesIssuetype,
...booleanAttributes
} = data;
const {
@ -38,7 +39,9 @@ function parseDatasetToProps(data) {
mergeRequestEvents,
enableComments,
showJiraIssuesIntegration,
showJiraVulnerabilitiesIntegration,
enableJiraIssues,
enableJiraVulnerabilities,
gitlabIssuesEnabled,
} = parseBooleanInData(booleanAttributes);
@ -59,7 +62,10 @@ function parseDatasetToProps(data) {
},
jiraIssuesProps: {
showJiraIssuesIntegration,
showJiraVulnerabilitiesIntegration,
initialEnableJiraIssues: enableJiraIssues,
initialEnableJiraVulnerabilities: enableJiraVulnerabilities,
initialVulnerabilitiesIssuetype: vulnerabilitiesIssuetype,
initialProjectKey: projectKey,
gitlabIssuesEnabled,
upgradePlanPath,

View File

@ -26,3 +26,18 @@ export const fetchResetIntegration = ({ dispatch, getters }) => {
.then(() => dispatch('receiveResetIntegrationSuccess'))
.catch(() => dispatch('receiveResetIntegrationError'));
};
export const requestJiraIssueTypes = ({ commit }) => {
commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, '');
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, true);
};
export const receiveJiraIssueTypesSuccess = ({ commit }, issueTypes = []) => {
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
commit(types.SET_JIRA_ISSUE_TYPES, issueTypes);
};
export const receiveJiraIssueTypesError = ({ commit }, errorMessage) => {
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
commit(types.SET_JIRA_ISSUE_TYPES, []);
commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, errorMessage);
};

View File

@ -3,5 +3,9 @@ export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';
export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES';
export const SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE = 'SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE';
export const SET_JIRA_ISSUE_TYPES = 'SET_JIRA_ISSUE_TYPES';
export const REQUEST_RESET_INTEGRATION = 'REQUEST_RESET_INTEGRATION';
export const RECEIVE_RESET_INTEGRATION_ERROR = 'RECEIVE_RESET_INTEGRATION_ERROR';

View File

@ -19,4 +19,13 @@ export default {
[types.RECEIVE_RESET_INTEGRATION_ERROR](state) {
state.isResetting = false;
},
[types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes) {
state.jiraIssueTypes = jiraIssueTypes;
},
[types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoadingJiraIssueTypes) {
state.isLoadingJiraIssueTypes = isLoadingJiraIssueTypes;
},
[types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage) {
state.loadingJiraIssueTypesErrorMessage = errorMessage;
},
};

View File

@ -8,5 +8,8 @@ export default ({ defaultState = null, customState = {} } = {}) => {
isSaving: false,
isTesting: false,
isResetting: false,
isLoadingJiraIssueTypes: false,
loadingJiraIssueTypesErrorMessage: '',
jiraIssueTypes: [],
};
};

View File

@ -33,6 +33,12 @@ export default class IntegrationSettingsForm {
eventHub.$on('saveIntegration', () => {
this.saveIntegration();
});
eventHub.$on('getJiraIssueTypes', () => {
// eslint-disable-next-line no-jquery/no-serialize
this.getJiraIssueTypes(this.$form.serialize());
});
eventHub.$emit('formInitialized');
}
saveIntegration() {
@ -79,16 +85,59 @@ export default class IntegrationSettingsForm {
}
}
/**
* Get a list of Jira issue types for the currently configured project
*
* @param {string} formData - URL encoded string containing the form data
*
* @return {Promise}
*/
getJiraIssueTypes(formData) {
const {
$store: { dispatch },
} = this.vue;
dispatch('requestJiraIssueTypes');
return this.fetchTestSettings(formData)
.then(
({
data: {
issuetypes,
error,
message = s__('Integrations|Connection failed. Please check your settings.'),
},
}) => {
if (error || !issuetypes?.length) {
eventHub.$emit('validateForm');
throw new Error(message);
}
dispatch('receiveJiraIssueTypesSuccess', issuetypes);
},
)
.catch(({ message = __('Something went wrong on our end.') }) => {
dispatch('receiveJiraIssueTypesError', message);
});
}
/**
* Send request to the test endpoint which checks if the current config is valid
*/
fetchTestSettings(formData) {
return axios.put(this.testEndPoint, formData);
}
/**
* Test Integration config
*/
testSettings(formData) {
return axios
.put(this.testEndPoint, formData)
return this.fetchTestSettings(formData)
.then(({ data }) => {
if (data.error) {
toast(`${data.message} ${data.service_response}`);
} else {
this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
toast(s__('Integrations|Connection successful.'));
}
})

View File

@ -16091,15 +16091,27 @@ msgstr ""
msgid "JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}"
msgstr ""
msgid "JiraService|An error occured while fetching issue list"
msgstr ""
msgid "JiraService|Define the type of Jira issue to create from a vulnerability."
msgstr ""
msgid "JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they wont otherwise be used."
msgstr ""
msgid "JiraService|Enable Jira issues"
msgstr ""
msgid "JiraService|Enable Jira issues creation from vulnerabilities"
msgstr ""
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
msgid "JiraService|Fetch issue types for this Jira project"
msgstr ""
msgid "JiraService|For example, 12, 24"
msgstr ""
@ -16109,6 +16121,9 @@ msgstr ""
msgid "JiraService|Issue List"
msgstr ""
msgid "JiraService|Issues created from vulnerabilities in this project will be Jira issues, even if GitLab issues are enabled."
msgstr ""
msgid "JiraService|Jira API URL"
msgstr ""
@ -16124,6 +16139,9 @@ msgstr ""
msgid "JiraService|Jira issue tracker"
msgstr ""
msgid "JiraService|Jira issue type"
msgstr ""
msgid "JiraService|Jira project key"
msgstr ""
@ -16136,6 +16154,15 @@ msgstr ""
msgid "JiraService|Password or API token"
msgstr ""
msgid "JiraService|Project key changed, refresh list"
msgstr ""
msgid "JiraService|Project key is required to generate issue types"
msgstr ""
msgid "JiraService|Select issue type"
msgstr ""
msgid "JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}"
msgstr ""

View File

@ -3,18 +3,22 @@ import { mount } from '@vue/test-utils';
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import eventHub from '~/integrations/edit/event_hub';
describe('JiraIssuesFields', () => {
let wrapper;
const defaultProps = {
showJiraIssuesIntegration: true,
editProjectPath: '/edit',
showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true,
};
const createComponent = (props) => {
const createComponent = ({ props, ...options } = {}) => {
wrapper = mount(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
stubs: ['jira-issue-creation-vulnerabilities'],
...options,
});
};
@ -28,11 +32,14 @@ describe('JiraIssuesFields', () => {
const findEnableCheckbox = () => wrapper.find(GlFormCheckbox);
const findProjectKey = () => wrapper.find(GlFormInput);
const expectedBannerText = 'This is a Premium feature';
const findJiraForVulnerabilities = () => wrapper.find('[data-testid="jira-for-vulnerabilities"]');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
describe('template', () => {
describe('upgrade banner for non-Premium user', () => {
beforeEach(() => {
createComponent({ initialProjectKey: '', showJiraIssuesIntegration: false });
createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
});
it('shows upgrade banner', () => {
@ -47,7 +54,7 @@ describe('JiraIssuesFields', () => {
describe('Enable Jira issues checkbox', () => {
beforeEach(() => {
createComponent({ initialProjectKey: '' });
createComponent({ props: { initialProjectKey: '' } });
});
it('does not show upgrade banner', () => {
@ -69,20 +76,16 @@ describe('JiraIssuesFields', () => {
});
describe('on enable issues', () => {
it('enables project_key input', () => {
findEnableCheckbox().vm.$emit('input', true);
it('enables project_key input', async () => {
await setEnableCheckbox(true);
return wrapper.vm.$nextTick().then(() => {
expect(findProjectKey().attributes('disabled')).toBeUndefined();
});
expect(findProjectKey().attributes('disabled')).toBeUndefined();
});
it('requires project_key input', () => {
findEnableCheckbox().vm.$emit('input', true);
it('requires project_key input', async () => {
await setEnableCheckbox(true);
return wrapper.vm.$nextTick().then(() => {
expect(findProjectKey().attributes('required')).toBe('required');
});
expect(findProjectKey().attributes('required')).toBe('required');
});
});
});
@ -103,10 +106,46 @@ describe('JiraIssuesFields', () => {
});
it('does not contain warning when GitLab issues is disabled', () => {
createComponent({ gitlabIssuesEnabled: false });
createComponent({ props: { gitlabIssuesEnabled: false } });
expect(wrapper.text()).not.toContain(expectedText);
});
});
describe('Vulnerabilities creation', () => {
beforeEach(() => {
createComponent({ provide: { glFeatures: { jiraForVulnerabilities: true } } });
});
it.each([true, false])(
'shows the jira-vulnerabilities component correctly when jira issues enables is set to "%s"',
async (hasJiraIssuesEnabled) => {
await setEnableCheckbox(hasJiraIssuesEnabled);
expect(findJiraForVulnerabilities().exists()).toBe(hasJiraIssuesEnabled);
},
);
it('emits "getJiraIssueTypes" to the eventHub when the jira-vulnerabilities component requests to fetch issue types', async () => {
const eventHubEmitSpy = jest.spyOn(eventHub, '$emit');
await setEnableCheckbox(true);
await findJiraForVulnerabilities().vm.$emit('request-get-issue-types');
expect(eventHubEmitSpy).toHaveBeenCalledWith('getJiraIssueTypes');
});
describe('with "jiraForVulnerabilities" feature flag disabled', () => {
beforeEach(async () => {
createComponent({
provide: { glFeatures: { jiraForVulnerabilities: false } },
});
});
it('does not show section', () => {
expect(findJiraForVulnerabilities().exists()).toBe(false);
});
});
});
});
});

View File

@ -9,6 +9,9 @@ import {
requestResetIntegration,
receiveResetIntegrationSuccess,
receiveResetIntegrationError,
requestJiraIssueTypes,
receiveJiraIssueTypesSuccess,
receiveJiraIssueTypesError,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
@ -70,4 +73,34 @@ describe('Integration form store actions', () => {
]);
});
});
describe('requestJiraIssueTypes', () => {
it('should commit SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE and SET_IS_LOADING_JIRA_ISSUE_TYPES mutations', () => {
return testAction(requestJiraIssueTypes, null, state, [
{ type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: '' },
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: true },
]);
});
});
describe('receiveJiraIssueTypesSuccess', () => {
it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES mutations', () => {
const issueTypes = ['issue', 'epic'];
return testAction(receiveJiraIssueTypesSuccess, issueTypes, state, [
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
{ type: types.SET_JIRA_ISSUE_TYPES, payload: issueTypes },
]);
});
});
describe('receiveJiraIssueTypesError', () => {
it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES, SET_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE mutations', () => {
const errorMessage = 'something went wrong';
return testAction(receiveJiraIssueTypesError, errorMessage, state, [
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
{ type: types.SET_JIRA_ISSUE_TYPES, payload: [] },
{ type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: errorMessage },
]);
});
});
});

View File

@ -56,4 +56,30 @@ describe('Integration form store mutations', () => {
expect(state.isResetting).toBe(false);
});
});
describe(`${types.SET_JIRA_ISSUE_TYPES}`, () => {
it('sets jiraIssueTypes', () => {
const jiraIssueTypes = ['issue', 'epic'];
mutations[types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes);
expect(state.jiraIssueTypes).toBe(jiraIssueTypes);
});
});
describe(`${types.SET_IS_LOADING_JIRA_ISSUE_TYPES}`, () => {
it.each([true, false])('sets isLoadingJiraIssueTypes to "%s"', (isLoading) => {
mutations[types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoading);
expect(state.isLoadingJiraIssueTypes).toBe(isLoading);
});
});
describe(`${types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE}`, () => {
it('sets loadingJiraIssueTypesErrorMessage', () => {
const errorMessage = 'something went wrong';
mutations[types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage);
expect(state.loadingJiraIssueTypesErrorMessage).toBe(errorMessage);
});
});
});

View File

@ -9,6 +9,9 @@ describe('Integration form state factory', () => {
isTesting: false,
isResetting: false,
override: false,
isLoadingJiraIssueTypes: false,
jiraIssueTypes: [],
loadingJiraIssueTypesErrorMessage: '',
});
});

View File

@ -132,4 +132,83 @@ describe('IntegrationSettingsForm', () => {
expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
});
});
describe('getJiraIssueTypes', () => {
let integrationSettingsForm;
let formData;
let mock;
beforeEach(() => {
mock = new MockAdaptor(axios);
jest.spyOn(axios, 'put');
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
// eslint-disable-next-line no-jquery/no-serialize
formData = integrationSettingsForm.$form.serialize();
});
afterEach(() => {
mock.restore();
});
it('should always dispatch `requestJiraIssueTypes`', async () => {
const dispatchSpy = jest.fn();
mock.onPut(integrationSettingsForm.testEndPoint).networkError();
integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
await integrationSettingsForm.getJiraIssueTypes();
expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
});
it('should make an ajax request with provided `formData`', async () => {
await integrationSettingsForm.getJiraIssueTypes(formData);
expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
});
it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
const mockData = ['ISSUE', 'EPIC'];
const dispatchSpy = jest.fn();
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: false,
issuetypes: mockData,
});
integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
await integrationSettingsForm.getJiraIssueTypes(formData);
expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
});
it.each(['something went wrong', undefined])(
'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
async (responseErrorMessage) => {
const defaultErrorMessage = 'Connection failed. Please check your settings.';
const expectedErrorMessage = responseErrorMessage || defaultErrorMessage;
const dispatchSpy = jest.fn();
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: responseErrorMessage,
});
integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
await integrationSettingsForm.getJiraIssueTypes(formData);
expect(dispatchSpy).toHaveBeenCalledWith(
'receiveJiraIssueTypesError',
expectedErrorMessage,
);
},
);
});
});