Commit 396166ae authored by Mark Florian's avatar Mark Florian

Merge branch '284784-migrate-access-dropdown-for-edit-environments-access' into 'master'

Migrate protected environments's edit access dropdown to GlDropdown

See merge request gitlab-org/gitlab!71119
parents 060cc3dc 6582038a
import * as Sentry from '@sentry/browser';
import Vue from 'vue';
import AccessDropdown from './components/access_dropdown.vue';
......@@ -7,6 +8,13 @@ export const initAccessDropdown = (el, options) => {
}
const { accessLevelsData, accessLevel } = options;
const { label, disabled, preselectedItems } = el.dataset;
let preselected = [];
try {
preselected = JSON.parse(preselectedItems);
} catch (e) {
Sentry.captureException(e);
}
return new Vue({
el,
......@@ -16,6 +24,9 @@ export const initAccessDropdown = (el, options) => {
props: {
accessLevel,
accessLevelsData: accessLevelsData.roles,
preselectedItems: preselected,
label,
disabled,
},
on: {
select(selected) {
......
import Vue from 'vue';
import ProtectedEnvironmentCreate from 'ee/protected_environments/protected_environment_create';
import ProtectedEnvironmentEditList from 'ee/protected_environments/protected_environment_edit_list';
import { initProtectedEnvironmentEditList } from 'ee/protected_environments/protected_environment_edit_list';
import LicenseManagement from 'ee/vue_shared/license_compliance/license_management.vue';
import createStore from 'ee/vue_shared/license_compliance/store/index';
import showToast from '~/vue_shared/plugins/global_toast';
......@@ -32,5 +32,4 @@ toasts.forEach((toast) => showToast(toast.dataset.message));
// eslint-disable-next-line no-new
new ProtectedEnvironmentCreate();
// eslint-disable-next-line no-new
new ProtectedEnvironmentEditList();
initProtectedEnvironmentEditList();
......@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import { ACCESS_LEVELS } from './constants';
const PROTECTED_ENVIRONMENT_INPUT = 'input[name="protected_environment[name]"]';
......@@ -84,29 +84,7 @@ export default class ProtectedEnvironmentCreate {
name: this.$form.find(PROTECTED_ENVIRONMENT_INPUT).val(),
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevel = ACCESS_LEVELS[level];
const levelAttributes = [];
this.selected.forEach((item) => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.id,
});
} else if (item.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: item.id,
});
} else if (item.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: item.id,
});
}
});
formData.protected_environment[`${accessLevel}_attributes`] = levelAttributes;
});
formData.protected_environment[`${ACCESS_LEVELS.DEPLOY}_attributes`] = this.selected;
return formData;
}
......
import $ from 'jquery';
import { find } from 'lodash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import AccessDropdown from '~/projects/settings/access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedEnvironmentEdit {
constructor(options) {
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToDeployDropdown = this.$wrap.find('.js-allowed-to-deploy');
this.$wraps[ACCESS_LEVELS.DEPLOY] = this.$allowedToDeployDropdown.closest(
`.${ACCESS_LEVELS.DEPLOY}-container`,
);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to deploy dropdown
this[`${ACCESS_LEVELS.DEPLOY}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.deploy,
accessLevelsData: gon.deploy_access_levels,
$dropdown: this.$allowedToDeployDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this),
});
}
onSelectOption() {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
return acc;
}, {});
axios
.patch(this.$wrap.data('url'), {
protected_environment: formData,
})
.then(({ data }) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevelName = ACCESS_LEVELS[level];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
});
this.$allowedToDeployDropdown.enable();
})
.catch(() => {
this.$allowedToDeployDropdown.enable();
createFlash({
message: __('Failed to update environment!'),
type: null,
parent: $('.js-protected-environments-list'),
});
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map((currentItem) => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
const selectedItems = this[dropdownName].getSelectedItems();
const currentSelectedItem = find(selectedItems, {
user_id: currentItem.user_id,
});
return {
id: currentItem.id,
user_id: currentItem.user_id,
type: LEVEL_TYPES.USER,
persisted: true,
name: currentSelectedItem.name,
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
} else if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true,
};
}
return {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true,
};
});
this[dropdownName].setSelectedItems(itemsToAdd);
}
}
<script>
import AccessDropdown from '~/projects/settings/components/access_dropdown.vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export const i18n = {
successMessage: __('Successfully updated the environment.'),
failureMessage: __('Failed to update environment!'),
};
export default {
i18n,
ACCESS_LEVELS,
accessLevelsData: gon?.deploy_access_levels?.roles ?? [],
components: {
AccessDropdown,
},
props: {
parentContainer: {
required: true,
type: HTMLElement,
},
url: {
type: String,
required: true,
},
label: {
type: String,
required: false,
default: i18n.selectUsers,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
preselectedItems: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
preselected: this.preselectedItems,
selected: null,
};
},
computed: {
hasChanges() {
return this.selected.some(({ id, _destroy }) => id === undefined || _destroy);
},
},
methods: {
updatePermissions(permissions) {
this.selected = permissions;
if (!this.hasChanges) {
return;
}
axios
.patch(this.url, {
protected_environment: { [`${ACCESS_LEVELS.DEPLOY}_attributes`]: permissions },
})
.then(({ data }) => {
this.$toast.show(i18n.successMessage);
this.updatePreselected(data);
})
.catch(() => {
createFlash({
message: i18n.failureMessage,
parent: this.parentContainer,
});
});
},
updatePreselected(items = []) {
this.preselected = items[ACCESS_LEVELS.DEPLOY].map(
({ id, user_id: userId, group_id: groupId, access_level: accessLevel }) => {
if (userId) {
return {
id,
user_id: userId,
type: LEVEL_TYPES.USER,
};
}
if (groupId) {
return {
id,
group_id: groupId,
type: LEVEL_TYPES.GROUP,
};
}
return {
id,
access_level: accessLevel,
type: LEVEL_TYPES.ROLE,
};
},
);
},
},
};
</script>
<template>
<access-dropdown
:access-levels-data="$options.accessLevelsData"
:access-level="$options.ACCESS_LEVELS.DEPLOY"
:label="label"
:disabled="disabled"
:preselected-items="preselected"
@hidden="updatePermissions"
/>
</template>
/* eslint-disable no-new */
import * as Sentry from '@sentry/browser';
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import ProtectedEnvironmentEdit from './protected_environment_edit.vue';
import $ from 'jquery';
import ProtectedEnvironmentEdit from './protected_environment_edit';
Vue.use(GlToast);
export default class ProtectedEnvironmentEditList {
constructor() {
this.$wrap = $('.protected-branches-list');
this.initEditForm();
}
export const initProtectedEnvironmentEditList = () => {
const parentContainer = document.querySelector('.js-protected-environments-list');
const envEditFormEls = parentContainer.querySelectorAll('.js-protected-environment-edit-form');
initEditForm() {
this.$wrap.find('.js-protected-environment-edit-form').each((i, el) => {
new ProtectedEnvironmentEdit({
$wrap: $(el),
});
envEditFormEls.forEach((el) => {
const accessDropdownEl = el.querySelector('.js-allowed-to-deploy');
if (!accessDropdownEl) {
return false;
}
const { url } = el.dataset;
const { label, disabled, preselectedItems } = accessDropdownEl.dataset;
let preselected = [];
try {
preselected = JSON.parse(preselectedItems);
} catch (e) {
Sentry.captureException(e);
}
return new Vue({
el: accessDropdownEl,
render(createElement) {
return createElement(ProtectedEnvironmentEdit, {
props: {
parentContainer,
preselectedItems: preselected,
url,
label,
disabled,
},
});
},
});
}
}
});
};
......@@ -3,6 +3,7 @@
%p.settings-message.text-center
= s_('ProtectedEnvironment|There are currently no protected environments. Protect an environment with this form.')
- else
.flash-container
%table.table.table-bordered
%colgroup
%col{ width: '30%' }
......
......@@ -11,7 +11,7 @@
= render partial: 'projects/protected_environments/environments_dropdown', locals: { f: f, project: @project }
.form-group
%label#allowed-users-label.label-bold
%label#allowed-users-label.label-bold.gl-display-block
= s_('ProtectedEnvironment|Allowed to deploy')
.js-allowed-to-deploy-dropdown
......
- default_label = s_('RepositorySettingsAccessLevel|Select')
.deploy_access_levels-container
= dropdown_tag(default_label, options: { toggle_class: 'js-allowed-to-deploy wide js-multiselect', disabled: local_assigns[:disabled], dropdown_class: 'dropdown-menu-selectable dropdown-menu-user', filter: true, data: { field_name: "allowed_to_deploy_#{protected_environment.id}", preselected_items: access_levels_data(access_levels) }})
.js-allowed-to-deploy{ data: {label: default_label, disabled: local_assigns[:disabled], preselected_items: access_levels_data(access_levels).to_json } }
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import AccessDropdown from '~/projects/settings/components/access_dropdown.vue';
import ProtectedEnvironmentEdit, {
i18n,
} from 'ee/protected_environments/protected_environment_edit.vue';
import { ACCESS_LEVELS, LEVEL_TYPES } from 'ee/protected_environments/constants';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
jest.mock('~/flash');
const $toast = {
show: jest.fn(),
};
describe('Protected Environment Edit', () => {
let wrapper;
let originalGon;
let mockAxios;
const url = 'http://some.url';
const parentContainer = document.createElement('div');
beforeEach(() => {
originalGon = window.gon;
window.gon = {
...window.gon,
deploy_access_levels: {
roles: [],
},
};
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
window.gon = originalGon;
mockAxios.restore();
wrapper.destroy();
});
const createComponent = ({ preselectedItems = [], disabled = false, label = '' } = {}) => {
wrapper = shallowMount(ProtectedEnvironmentEdit, {
propsData: {
parentContainer,
url,
disabled,
label,
preselectedItems,
},
mocks: {
$toast,
},
});
};
const findAccessDropdown = () => wrapper.findComponent(AccessDropdown);
it('renders AccessDropdown and passes down the props', () => {
const label = 'Update permissions';
const disabled = true;
const preselectedItems = [1, 2, 3];
createComponent({
label,
disabled,
preselectedItems,
});
const dropdown = findAccessDropdown();
expect(dropdown.props()).toMatchObject({
accessLevel: ACCESS_LEVELS.DEPLOY,
disabled,
label,
preselectedItems,
});
});
it('should NOT make a request if updated permissions are the same as preselected', () => {
createComponent();
jest.spyOn(axios, 'patch');
findAccessDropdown().vm.$emit('hidden', []);
expect(axios.patch).not.toHaveBeenCalled();
});
it('should make a request if updated permissions are different than preselected', () => {
createComponent();
jest.spyOn(axios, 'patch');
const newPermissions = [{ user_id: 1 }];
findAccessDropdown().vm.$emit('hidden', newPermissions);
expect(axios.patch).toHaveBeenCalledWith(url, {
protected_environment: { deploy_access_levels_attributes: newPermissions },
});
});
describe('on successful permissions update', () => {
beforeEach(async () => {
createComponent();
const updatedPermissions = [
{ user_id: 1, id: 1 },
{ group_id: 1, id: 2 },
{ access_level: 3, id: 3 },
];
mockAxios
.onPatch()
.replyOnce(httpStatusCodes.OK, { [ACCESS_LEVELS.DEPLOY]: updatedPermissions });
findAccessDropdown().vm.$emit('hidden', [{ user_id: 1 }]);
await waitForPromises();
});
it('should show a toast with success message', () => {
expect($toast.show).toHaveBeenCalledWith(i18n.successMessage);
});
it('should update preselected', () => {
const newPreselected = [
{ user_id: 1, id: 1, type: LEVEL_TYPES.USER },
{ group_id: 1, id: 2, type: LEVEL_TYPES.GROUP },
{ access_level: 3, id: 3, type: LEVEL_TYPES.ROLE },
];
expect(findAccessDropdown().props('preselectedItems')).toEqual(newPreselected);
});
});
describe('on permissions update failure', () => {
beforeEach(() => {
mockAxios.onPatch().replyOnce(httpStatusCodes.BAD_REQUEST, {});
createComponent();
});
it('should show error message', async () => {
findAccessDropdown().vm.$emit('hidden', [{ user_id: 1 }]);
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: i18n.failureMessage,
parent: parentContainer,
});
});
});
});
......@@ -32807,6 +32807,9 @@ msgstr ""
msgid "Successfully updated %{last_updated_timeago}."
msgstr ""
msgid "Successfully updated the environment."
msgstr ""
msgid "Suggest code changes which can be immediately applied in one click. Try it out!"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment