Commit 6582038a authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Mark Florian

Styling updates of the protected environments edit access dropdown

Due to migration to GlDropdown.

Also, migrate `protected_environment_edit` to Vue. This is  done
for easier communication between the components. Besides the
migration would eventually happen anyways.

Part of https://gitlab.com/gitlab-org/gitlab/-/issues/284784.

Changelog: changed
EE: true
parent dd4410f0
import * as Sentry from '@sentry/browser';
import Vue from 'vue'; import Vue from 'vue';
import AccessDropdown from './components/access_dropdown.vue'; import AccessDropdown from './components/access_dropdown.vue';
...@@ -7,6 +8,13 @@ export const initAccessDropdown = (el, options) => { ...@@ -7,6 +8,13 @@ export const initAccessDropdown = (el, options) => {
} }
const { accessLevelsData, accessLevel } = 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({ return new Vue({
el, el,
...@@ -16,6 +24,9 @@ export const initAccessDropdown = (el, options) => { ...@@ -16,6 +24,9 @@ export const initAccessDropdown = (el, options) => {
props: { props: {
accessLevel, accessLevel,
accessLevelsData: accessLevelsData.roles, accessLevelsData: accessLevelsData.roles,
preselectedItems: preselected,
label,
disabled,
}, },
on: { on: {
select(selected) { select(selected) {
......
import Vue from 'vue'; import Vue from 'vue';
import ProtectedEnvironmentCreate from 'ee/protected_environments/protected_environment_create'; 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 LicenseManagement from 'ee/vue_shared/license_compliance/license_management.vue';
import createStore from 'ee/vue_shared/license_compliance/store/index'; import createStore from 'ee/vue_shared/license_compliance/store/index';
import showToast from '~/vue_shared/plugins/global_toast'; import showToast from '~/vue_shared/plugins/global_toast';
...@@ -32,5 +32,4 @@ toasts.forEach((toast) => showToast(toast.dataset.message)); ...@@ -32,5 +32,4 @@ toasts.forEach((toast) => showToast(toast.dataset.message));
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new ProtectedEnvironmentCreate(); new ProtectedEnvironmentCreate();
// eslint-disable-next-line no-new initProtectedEnvironmentEditList();
new ProtectedEnvironmentEditList();
...@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor'; ...@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown'; 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]"]'; const PROTECTED_ENVIRONMENT_INPUT = 'input[name="protected_environment[name]"]';
...@@ -84,29 +84,7 @@ export default class ProtectedEnvironmentCreate { ...@@ -84,29 +84,7 @@ export default class ProtectedEnvironmentCreate {
name: this.$form.find(PROTECTED_ENVIRONMENT_INPUT).val(), name: this.$form.find(PROTECTED_ENVIRONMENT_INPUT).val(),
}, },
}; };
formData.protected_environment[`${ACCESS_LEVELS.DEPLOY}_attributes`] = this.selected;
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;
});
return formData; 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'; Vue.use(GlToast);
import ProtectedEnvironmentEdit from './protected_environment_edit';
export default class ProtectedEnvironmentEditList { export const initProtectedEnvironmentEditList = () => {
constructor() { const parentContainer = document.querySelector('.js-protected-environments-list');
this.$wrap = $('.protected-branches-list'); const envEditFormEls = parentContainer.querySelectorAll('.js-protected-environment-edit-form');
this.initEditForm();
envEditFormEls.forEach((el) => {
const accessDropdownEl = el.querySelector('.js-allowed-to-deploy');
if (!accessDropdownEl) {
return false;
} }
initEditForm() { const { url } = el.dataset;
this.$wrap.find('.js-protected-environment-edit-form').each((i, el) => { const { label, disabled, preselectedItems } = accessDropdownEl.dataset;
new ProtectedEnvironmentEdit({
$wrap: $(el), 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 @@ ...@@ -3,6 +3,7 @@
%p.settings-message.text-center %p.settings-message.text-center
= s_('ProtectedEnvironment|There are currently no protected environments. Protect an environment with this form.') = s_('ProtectedEnvironment|There are currently no protected environments. Protect an environment with this form.')
- else - else
.flash-container
%table.table.table-bordered %table.table.table-bordered
%colgroup %colgroup
%col{ width: '30%' } %col{ width: '30%' }
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
= render partial: 'projects/protected_environments/environments_dropdown', locals: { f: f, project: @project } = render partial: 'projects/protected_environments/environments_dropdown', locals: { f: f, project: @project }
.form-group .form-group
%label#allowed-users-label.label-bold %label#allowed-users-label.label-bold.gl-display-block
= s_('ProtectedEnvironment|Allowed to deploy') = s_('ProtectedEnvironment|Allowed to deploy')
.js-allowed-to-deploy-dropdown .js-allowed-to-deploy-dropdown
......
- default_label = s_('RepositorySettingsAccessLevel|Select') - default_label = s_('RepositorySettingsAccessLevel|Select')
.deploy_access_levels-container .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,
});
});
});
});
...@@ -32804,6 +32804,9 @@ msgstr "" ...@@ -32804,6 +32804,9 @@ msgstr ""
msgid "Successfully updated %{last_updated_timeago}." msgid "Successfully updated %{last_updated_timeago}."
msgstr "" msgstr ""
msgid "Successfully updated the environment."
msgstr ""
msgid "Suggest code changes which can be immediately applied in one click. Try it out!" msgid "Suggest code changes which can be immediately applied in one click. Try it out!"
msgstr "" 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