Commit 772911ad authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'paginate-license-management' into 'master'

Paginate license management

See merge request gitlab-org/gitlab-ee!10983
parents 8346651f bd685e53
<script>
import { GlPaginatedList } from '@gitlab/ui';
import { PREV, NEXT } from '~/vue_shared/components/pagination/constants';
export default {
components: {
GlPaginatedList,
},
labels: {
prev: PREV,
next: NEXT,
},
};
</script>
<template>
<gl-paginated-list
v-bind="$attrs"
:prev-text="$options.labels.prev"
:next-text="$options.labels.next"
>
<!-- proxy the slots -->
<template #header>
<slot name="header"></slot>
</template>
<template #subheader>
<slot name="subheader"></slot>
</template>
<template #default="{ listItem, query }">
<slot :listItem="listItem" :query="query"></slot>
</template>
</gl-paginated-list>
</template>
...@@ -262,6 +262,8 @@ To approve or blacklist a license: ...@@ -262,6 +262,8 @@ To approve or blacklist a license:
navigate to the project's **Settings > CI/CD** and expand the navigate to the project's **Settings > CI/CD** and expand the
**License Management** section. **License Management** section.
1. Click the **Add a license** button. 1. Click the **Add a license** button.
![License Management Add License](img/license_management_add_license.png)
1. In the **License name** dropdown, either: 1. In the **License name** dropdown, either:
- Select one of the available licenses. You can search for licenses in the field - Select one of the available licenses. You can search for licenses in the field
at the top of the list. at the top of the list.
...@@ -270,8 +272,22 @@ To approve or blacklist a license: ...@@ -270,8 +272,22 @@ To approve or blacklist a license:
1. Select the **Approve** or **Blacklist** radio button to approve or blacklist respectively 1. Select the **Approve** or **Blacklist** radio button to approve or blacklist respectively
the selected license. the selected license.
To modify an existing license:
1. In the **License Management** list, click the **Approved/Declined** dropdown to change it to the desired status.
![License Management Settings](img/license_management_settings.png) ![License Management Settings](img/license_management_settings.png)
Searching for Licenses:
1. Use the **Search** box to search for a specific license.
![License Management Search](img/license_management_search.png)
## License Management report under pipelines ## License Management report under pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5491) > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5491)
......
...@@ -55,7 +55,7 @@ export default { ...@@ -55,7 +55,7 @@ export default {
}; };
</script> </script>
<template> <template>
<li class="list-group-item"> <div>
<issue-status-icon :status="status" class="float-left append-right-default" /> <issue-status-icon :status="status" class="float-left append-right-default" />
<span class="js-license-name">{{ license.name }}</span> <span class="js-license-name">{{ license.name }}</span>
<div class="float-right"> <div class="float-right">
...@@ -85,5 +85,5 @@ export default { ...@@ -85,5 +85,5 @@ export default {
</button> </button>
</div> </div>
</div> </div>
</li> </div>
</template> </template>
...@@ -5,6 +5,7 @@ import { s__ } from '~/locale'; ...@@ -5,6 +5,7 @@ import { s__ } from '~/locale';
import AddLicenseForm from './components/add_license_form.vue'; import AddLicenseForm from './components/add_license_form.vue';
import LicenseManagementRow from './components/license_management_row.vue'; import LicenseManagementRow from './components/license_management_row.vue';
import DeleteConfirmationModal from './components/delete_confirmation_modal.vue'; import DeleteConfirmationModal from './components/delete_confirmation_modal.vue';
import PaginatedList from '~/vue_shared/components/paginated_list.vue';
import createStore from './store/index'; import createStore from './store/index';
const store = createStore(); const store = createStore();
...@@ -17,6 +18,7 @@ export default { ...@@ -17,6 +18,7 @@ export default {
LicenseManagementRow, LicenseManagementRow,
GlButton, GlButton,
GlLoadingIcon, GlLoadingIcon,
PaginatedList,
}, },
props: { props: {
apiUrl: { apiUrl: {
...@@ -28,9 +30,6 @@ export default { ...@@ -28,9 +30,6 @@ export default {
return { formIsOpen: false }; return { formIsOpen: false };
}, },
store, store,
emptyMessage: s__(
'LicenseManagement|There are currently no approved or blacklisted licenses in this project.',
),
computed: { computed: {
...mapState(['managedLicenses', 'isLoadingManagedLicenses']), ...mapState(['managedLicenses', 'isLoadingManagedLicenses']),
}, },
...@@ -49,30 +48,49 @@ export default { ...@@ -49,30 +48,49 @@ export default {
this.formIsOpen = false; this.formIsOpen = false;
}, },
}, },
emptyMessage: s__(
'LicenseManagement|There are currently no approved or blacklisted licenses in this project.',
),
emptySearchMessage: s__(
'LicenseManagement|There are currently no approved or blacklisted licenses that match in this project.',
),
}; };
</script> </script>
<template> <template>
<div class="license-management"> <gl-loading-icon v-if="isLoadingManagedLicenses" />
<div v-else class="license-management">
<delete-confirmation-modal /> <delete-confirmation-modal />
<gl-loading-icon v-if="isLoadingManagedLicenses" />
<ul v-if="managedLicenses.length" class="list-group list-group-flush"> <paginated-list
<license-management-row :list="managedLicenses"
v-for="license in managedLicenses" :empty-search-message="$options.emptySearchMessage"
:key="license.name" :empty-message="$options.emptyMessage"
:license="license" filter="name"
/> >
</ul> <template #header>
<div v-else class="bs-callout bs-callout-warning">{{ $options.emptyMessage }}</div> <gl-button
<div class="prepend-top-default"> class="js-open-form order-1"
<add-license-form :disabled="formIsOpen"
v-if="formIsOpen" variant="success"
:managed-licenses="managedLicenses" @click="openAddLicenseForm"
@addLicense="setLicenseApproval" >
@closeForm="closeAddLicenseForm" {{ s__('LicenseManagement|Add a license') }}
/> </gl-button>
<gl-button v-else class="js-open-form" variant="default" @click="openAddLicenseForm"> </template>
{{ s__('LicenseManagement|Add a license') }}
</gl-button> <template #subheader>
</div> <div v-if="formIsOpen" class="prepend-top-default append-bottom-default">
<add-license-form
:managed-licenses="managedLicenses"
@addLicense="setLicenseApproval"
@closeForm="closeAddLicenseForm"
/>
</div>
</template>
<template #default="{ listItem }">
<license-management-row :license="listItem" />
</template>
</paginated-list>
</div> </div>
</template> </template>
import * as types from './mutation_types'; import * as types from './mutation_types';
import { normalizeLicense, byLicenseNameComparator } from './utils'; import { normalizeLicense } from './utils';
export default { export default {
[types.SET_LICENSE_IN_MODAL](state, license) { [types.SET_LICENSE_IN_MODAL](state, license) {
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
}, },
[types.RECEIVE_LOAD_MANAGED_LICENSES](state, licenses = []) { [types.RECEIVE_LOAD_MANAGED_LICENSES](state, licenses = []) {
const managedLicenses = licenses.map(normalizeLicense).sort(byLicenseNameComparator); const managedLicenses = licenses.map(normalizeLicense).reverse();
Object.assign(state, { Object.assign(state, {
managedLicenses, managedLicenses,
......
---
title: Paginate license management
merge_request: 10983
author:
type: added
...@@ -132,8 +132,8 @@ describe('LicenseManagementRow', () => { ...@@ -132,8 +132,8 @@ describe('LicenseManagementRow', () => {
}); });
describe('template', () => { describe('template', () => {
it('renders component container element with class `list-group-item`', () => { it('renders component container element as a div', () => {
expect(vm.$el.classList.contains('list-group-item')).toBe(true); expect(vm.$el.tagName).toBe('DIV');
}); });
it('renders status icon', () => { it('renders status icon', () => {
......
...@@ -9,25 +9,28 @@ import { approvedLicense, blacklistedLicense } from 'ee_spec/license_management/ ...@@ -9,25 +9,28 @@ import { approvedLicense, blacklistedLicense } from 'ee_spec/license_management/
describe('LicenseManagement', () => { describe('LicenseManagement', () => {
const Component = Vue.extend(LicenseManagement); const Component = Vue.extend(LicenseManagement);
const apiUrl = `${TEST_HOST}/license_management`; const apiUrl = `${TEST_HOST}/license_management`;
let vm; let vm;
let store;
let actions; let actions;
beforeEach(() => { const initVue = (mergeState = {}) => {
actions = { const store = new Vuex.Store({
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
};
store = new Vuex.Store({
state: { state: {
managedLicenses: [approvedLicense, blacklistedLicense], managedLicenses: [approvedLicense, blacklistedLicense],
currentLicenseInModal: approvedLicense, currentLicenseInModal: approvedLicense,
isLoadingManagedLicenses: true, isLoadingManagedLicenses: true,
...mergeState,
}, },
actions, actions,
}); });
vm = mountComponentWithStore(Component, { props: { apiUrl }, store }); return mountComponentWithStore(Component, { props: { apiUrl }, store });
};
beforeEach(() => {
actions = {
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
};
}); });
afterEach(() => { afterEach(() => {
...@@ -35,7 +38,8 @@ describe('LicenseManagement', () => { ...@@ -35,7 +38,8 @@ describe('LicenseManagement', () => {
}); });
describe('License Form', () => { describe('License Form', () => {
it('should render the form if the form is open', done => { it('should render the form if the form is open and disable the form button', done => {
vm = initVue({ isLoadingManagedLicenses: false });
vm.formIsOpen = true; vm.formIsOpen = true;
return Vue.nextTick() return Vue.nextTick()
...@@ -45,13 +49,14 @@ describe('LicenseManagement', () => { ...@@ -45,13 +49,14 @@ describe('LicenseManagement', () => {
expect(formEl).not.toBeNull(); expect(formEl).not.toBeNull();
const buttonEl = vm.$el.querySelector('.js-open-form'); const buttonEl = vm.$el.querySelector('.js-open-form');
expect(buttonEl).toBeNull(); expect(buttonEl).toHaveClass('disabled');
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('should render the button if the form is closed', done => { it('should render the button if the form is closed', done => {
vm = initVue({ isLoadingManagedLicenses: false });
vm.formIsOpen = false; vm.formIsOpen = false;
return Vue.nextTick() return Vue.nextTick()
...@@ -67,30 +72,37 @@ describe('LicenseManagement', () => { ...@@ -67,30 +72,37 @@ describe('LicenseManagement', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('clicking the Add a license button opens the form', () => { it('clicking the Add a license button opens the form', done => {
const linkEl = vm.$el.querySelector('.js-open-form'); vm = initVue({ isLoadingManagedLicenses: false });
expect(vm.formIsOpen).toBe(false); return Vue.nextTick()
.then(() => {
const linkEl = vm.$el.querySelector('.js-open-form');
linkEl.click(); expect(vm.formIsOpen).toBe(false);
expect(vm.formIsOpen).toBe(true); linkEl.click();
expect(vm.formIsOpen).toBe(true);
done();
})
.catch(done.fail);
}); });
}); });
it('should render loading icon', done => { it('should render loading icon', done => {
store.replaceState({ ...store.state, isLoadingManagedLicenses: true }); vm = initVue({ isLoadingManagedLicenses: true });
return Vue.nextTick() return Vue.nextTick()
.then(() => { .then(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBeNull(); expect(vm.$el.classList.contains('loading-container')).toEqual(true);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('should render callout if no licenses are managed', done => { it('should render callout if no licenses are managed', done => {
store.replaceState({ ...store.state, managedLicenses: [], isLoadingManagedLicenses: false }); vm = initVue({ managedLicenses: [], isLoadingManagedLicenses: false });
return Vue.nextTick() return Vue.nextTick()
.then(() => { .then(() => {
...@@ -104,7 +116,7 @@ describe('LicenseManagement', () => { ...@@ -104,7 +116,7 @@ describe('LicenseManagement', () => {
}); });
it('should render delete confirmation modal', done => { it('should render delete confirmation modal', done => {
store.replaceState({ ...store.state }); vm = initVue({ isLoadingManagedLicenses: false });
return Vue.nextTick() return Vue.nextTick()
.then(() => { .then(() => {
...@@ -115,7 +127,7 @@ describe('LicenseManagement', () => { ...@@ -115,7 +127,7 @@ describe('LicenseManagement', () => {
}); });
it('should render list of managed licenses', done => { it('should render list of managed licenses', done => {
store.replaceState({ ...store.state, isLoadingManagedLicenses: false }); vm = initVue({ isLoadingManagedLicenses: false });
return Vue.nextTick() return Vue.nextTick()
.then(() => { .then(() => {
...@@ -127,18 +139,24 @@ describe('LicenseManagement', () => { ...@@ -127,18 +139,24 @@ describe('LicenseManagement', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('should set api settings after mount and init API calls', () => it('should set api settings after mount and init API calls', done => {
Vue.nextTick().then(() => { vm = initVue();
expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object), return Vue.nextTick()
{ apiUrlManageLicenses: apiUrl }, .then(() => {
undefined, expect(actions.setAPISettings).toHaveBeenCalledWith(
); jasmine.any(Object),
{ apiUrlManageLicenses: apiUrl },
expect(actions.loadManagedLicenses).toHaveBeenCalledWith( undefined,
jasmine.any(Object), );
undefined,
undefined, expect(actions.loadManagedLicenses).toHaveBeenCalledWith(
); jasmine.any(Object),
})); undefined,
undefined,
);
done();
})
.catch(done.fail);
});
}); });
...@@ -7826,6 +7826,9 @@ msgstr "" ...@@ -7826,6 +7826,9 @@ msgstr ""
msgid "LicenseManagement|There are currently no approved or blacklisted licenses in this project." msgid "LicenseManagement|There are currently no approved or blacklisted licenses in this project."
msgstr "" msgstr ""
msgid "LicenseManagement|There are currently no approved or blacklisted licenses that match in this project."
msgstr ""
msgid "LicenseManagement|This license already exists in this project." msgid "LicenseManagement|This license already exists in this project."
msgstr "" msgstr ""
......
import PaginatedList from '~/vue_shared/components/paginated_list.vue';
import { PREV, NEXT } from '~/vue_shared/components/pagination/constants';
import { mount } from '@vue/test-utils';
describe('Pagination links component', () => {
let wrapper;
let glPaginatedList;
const template = `
<div class="slot" slot-scope="{ listItem }">
<span class="item">Item Name: {{listItem.id}}</span>
</div>
`;
const props = {
prevText: PREV,
nextText: NEXT,
};
beforeEach(() => {
wrapper = mount(PaginatedList, {
scopedSlots: {
default: template,
},
propsData: {
list: [{ id: 'foo' }, { id: 'bar' }],
props,
},
});
[glPaginatedList] = wrapper.vm.$children;
});
afterEach(() => {
wrapper.destroy();
});
describe('Paginated List Component', () => {
describe('props', () => {
// We test attrs and not props because we pass through to child component using v-bind:"$attrs"
it('should pass prevText to GitLab UI paginated list', () => {
expect(glPaginatedList.$attrs['prev-text']).toBe(props.prevText);
});
it('should pass nextText to GitLab UI paginated list', () => {
expect(glPaginatedList.$attrs['next-text']).toBe(props.nextText);
});
});
describe('rendering', () => {
it('it renders the gl-paginated-list', () => {
expect(wrapper.contains('ul.list-group')).toBe(true);
expect(wrapper.findAll('li.list-group-item').length).toBe(2);
});
});
});
});
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